Extending a class without using the keyword "extends"

Post your working scripts, libraries and tools for AHK v1.1 and older
iseahound
Posts: 1446
Joined: 13 Aug 2016, 21:04
Contact:

Extending a class without using the keyword "extends"

22 Aug 2018, 09:17

Final Version - v2 compatible

Code: Select all

q := new uber.c
text := q.test1
_text := (A_AhkVersion < 2) ? text : "text"
MsgBox %_text%
q.test2()
q.test3()

; At each base "level" the name of the __class and the number of objects are displayed.
; The ground level should be an empty class with 2 objects.   [_extends, test1]
; The -1 (base) level should be the class "c" with 5 objects. [__Class, __extends, __Init, extends, outer]
; The -2 level should be the class "b" with 6 objects.        [__Class, __extends, __Init, extends, outer, test2]
; The -3 level should be the class "a" with 4 objects.        [__Class, __delete, __New, test3]
; The -4 level is invalid.
; Why does the ground level have an empty class? Because class names are like types - they live in obj.base.
; When a class is instansiated all the old functions go into its base object and its properties (_extends, test1) become the top level.
try level0 := "Ground Level: " ObjRawGet(q, "__class")                 "`nNumber of Objects: " q.Count()
try level1 := "Level -1: " ObjRawGet(q.base, "__class")                "`nNumber of Objects: " q.base.Count()
try level2 := "Level -2: " ObjRawGet(q.base.base, "__class")           "`nNumber of Objects: " q.base.base.Count()
try level3 := "Level -3: " ObjRawGet(q.base.base.base, "__class")      "`nNumber of Objects: " q.base.base.base.Count()
try level4 := "Level -4: " ObjRawGet(q.base.base.base.base, "__class") "`nNumber of Objects: " q.base.base.base.base.Count()

; v2 compatible code.
_level0 := (A_AhkVersion < 2) ? level0 : "level0"
_level1 := (A_AhkVersion < 2) ? level1 : "level1"
_level2 := (A_AhkVersion < 2) ? level2 : "level2"
_level3 := (A_AhkVersion < 2) ? level3 : "level3"
_level4 := (A_AhkVersion < 2) ? level4 : "level4"

; v2 compatible code.
try MsgBox %_level0%
try MsgBox %_level1%
try MsgBox %_level2%
try MsgBox %_level3%
try MsgBox %_level4%

; Necessary or the object will delete itself immediately.
; When the script exits the __delete method will be called.
Sleep 2500

class uber {

   class a {
      __New() {
         text := A_ThisFunc
         _text := (A_AhkVersion < 2) ? text : "text"
         MsgBox %_text%
      }

      __delete() {
         text := A_ThisFunc
         _text := (A_AhkVersion < 2) ? text : "text"
         MsgBox %_text%
      }

      test3() {
         text := "This is a method of class a."
         _text := (A_AhkVersion < 2) ? text : "text"
         MsgBox %_text%
      }
   }

   class b {
   static extends := "a"

      _extends := this.__extends()
      __extends(subbundle := "") {
         object := this.outer[this.extends]
         bundle := ((object.haskey("__extends")) ? object.__extends(true) : object)
         (subbundle) ? (this.base := bundle) : (this.base.base := bundle)
         return (subbundle) ? this : ""
      }

      outer[p:=""] {
         get {
            static period := ".", _period := (A_AhkVersion < 2) ? period : "period"
            if ((__outer := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
               Loop Parse, __outer, %_period%
                  outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
            return IsObject(outer) ? ((p) ? outer[p] : outer) : ((p) ? %p% : "")
         }
      }

      test2() {
         text := "This is a method of class b."
         _text := (A_AhkVersion < 2) ? text : "text"
         MsgBox %_text%
      }
   }

   class c {
   static extends := "b"

      _extends := this.__extends()
      __extends(subbundle := "") {
         object := this.outer[this.extends]
         bundle := ((object.haskey("__extends")) ? object.__extends(true) : object)
         (subbundle) ? (this.base := bundle) : (this.base.base := bundle)
         return (subbundle) ? this : ""
      }

      outer[p:=""] {
         get {
            static period := ".", _period := (A_AhkVersion < 2) ? period : "period"
            if ((__outer := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
               Loop Parse, __outer, %_period%
                  outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
            return IsObject(outer) ? ((p) ? outer[p] : outer) : ((p) ? %p% : "")
         }
      }

      test1 := "This is a property of class c."
   }
}

Original Post
Motivation: To create classes that can be nested or not nested.

Example #1 - Two classes are nested inside a bigger class, "uber"

Code: Select all

q := new uber.a()
MsgBox % q.red
q.test()

class uber {

   class a {

      red := "red"

      base := this.base
      base.base := (this.outer.b) ? this.outer.b : b  

      outer[] {
         get {
            if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
               Loop, Parse, _class, .
                  outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
            return outer
         }
      }
   }

   class b {
      test() {
         MsgBox % "This is the new extended class!"
      }
   }
}
Example #2 - Two classes are free and not nested.

Code: Select all

q := new a()
MsgBox % q.red
q.test()

class a {

   red := "red"

   base := this.base
   base.base := (this.outer.b) ? this.outer.b : b  

   outer[] {
      get {
         if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
            Loop, Parse, _class, .
               outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
         return outer
      }
   }
}

class b {

   test() {
      MsgBox % "This is the new extended class!"
   }
}
EDIT: this.outer => this.outer.b How did I not catch this error?
Last edited by iseahound on 02 Aug 2019, 21:10, edited 5 times in total.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Extending a class without using the keyword "extends"

22 Aug 2018, 09:28

thats an interesting effect you found with this there.
I think it is easier by overwriting the __init method (stuff like red has to be piut into the __Init method then)

Code: Select all

q := new a()
MsgBox % q.red
q.test()

class a {
	
	__init() {
		this.red := "red"
		a.base := b 
	}
}

class b {
	
	test() {
		MsgBox % "This is the new extended class!"
	}
}
Also I think this would reach more people in the Tips and Tricks forums and I would move it there if you want to.
Recommends AHK Studio
iseahound
Posts: 1446
Joined: 13 Aug 2016, 21:04
Contact:

Re: Extending a class without using the keyword "extends"

22 Aug 2018, 09:44

It's more of a abstraction than a trick. What we mean by "nestable" is the idea that classes nested together are of the same level or order. So I'm interested in the fact that this method extends a class relatively, as opposed to absolutely. (Think of relative and absolute file paths!)

EDIT: Yes I'm actually using this code in a script soon.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Extending a class without using the keyword "extends"

22 Aug 2018, 12:07

Hmm yeah I see thats indeed interesting.
This could be used for dynamically linking classes that can be included inside another class.
Recommends AHK Studio
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Extending a class without using the keyword "extends"

23 Aug 2018, 03:06

It is not very meaningful to set the base of a more than once. You are also limited to linking a to b, if you also want a second class c, to have the methods of a and c, it doesn't work. I've used a very simple function for linking general base classes, with example,

Code: Select all

extend(o, bases*) {
	; o extends bases*
	local
	for k, base in bases
		o := o.base := objclone(base)		
}
; My bases classes
class a {
	a() {
		return a_thisfunc
	}
}

class b {
	b() {
		return a_thisfunc
	}
}

class c {
	c() {
		return a_thisfunc
	}
}

; My classes
class abc {
}

class ac {
}

extend(abc, a, b, c)
extend(ac, a, c)

d := new abc
e := new ac

msgbox % d.a() "`n" d.b() "`n" d.c()
msgbox % e.a() "`n" e.c()
Unfortunately, this, and your methods are very limited because the __Init() method doesn't do base.__Init() unless you actually use the extends keyword, so it is easy to fix in nnnik's version, but that limits you in other ways. My function ofc has other limitations too.

I think these kinds of things are fun to tinker with, but ultimately, the code easily gets less maintainable imo.

Thanks for sharing, cheers.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Extending a class without using the keyword "extends"

23 Aug 2018, 04:40

Well it's not like I would attempt to inherit from multiple classes - that is bound to fail and makes it impossible to maintain scripts.
But if you link every class only to its own parent it will work.
Also the base := this.base does have an effect - just try it out.
Recommends AHK Studio
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Extending a class without using the keyword "extends"

23 Aug 2018, 05:58

Well it's not like I would attempt to inherit from multiple classes - that is bound to fail and makes it impossible to maintain scripts.
Indeed it cannot be done in a complete way, as I mentioned, there are limitations.
Also the base := this.base does have an effect - just try it out.
I know what it does, and it complies exactly with documented behaviour, behaviour which clearly isn't implemented for this particular purpose. Class body initialising is limited, hence why we have __new, I'd probably do it there instead tbh, something like,

Code: Select all

__new(){
	if !this.base.base
		this.base.base := (this.outer) ? this.outer.b : b  
	base.__init()
}
where it is also much easier to avoid invoking __get and __set if needed. Also avoids all unnecessary gets of the outer property every time you derive an object from the class.

Cheers.

Edit: As a side note, in v2 base := this.base is translated to objrawset(this, "base", (this.base)) so it will overwrite the base property.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Extending a class without using the keyword "extends"

23 Aug 2018, 06:52

Yeah ObjRawSet and ObjRawGet might be better here.
Regardless i think this has a use for specific types of libraries and avoids using global space.
Recommends AHK Studio
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Extending a class without using the keyword "extends"

23 Aug 2018, 08:29

nnnik wrote: Regardless i think this has a use for specific types of libraries and avoids using global space.
Yes I'm sure. I didn't mean to be too critical @iseahound.

Cheers :wave: .
iseahound
Posts: 1446
Joined: 13 Aug 2016, 21:04
Contact:

Re: Extending a class without using the keyword "extends"

24 Aug 2018, 01:29

I'm abstracting the __New() method as well, so the only logical place to put the boilerplate code is in __Init() or as an instance variable.

It's a coslice category mathematically.
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Extending a class without using the keyword "extends"

24 Aug 2018, 02:59

the only logical place to put the boilerplate code is in __Init() or as an instance variable.
Strictly speaking, __Init() is never the logical place to do anything, since the documentations says we shouldn't. Using a function might be more maintainable,

Code: Select all

f(o, p){
	local outer := "", _class
	if ((_class := RegExReplace(o.__class, "^(.*)\..*$", "$1")) != o.__class)
		Loop, Parse, _class, .
			outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
	o.base := outer ? outer[p] : %p%
}
Cheers.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Extending a class without using the keyword "extends"

25 Aug 2018, 11:58

a function then once again consumes global name space invalidating the original reason as to why we do this.
Recommends AHK Studio
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Extending a class without using the keyword "extends"

25 Aug 2018, 12:22

It's one function vs. potentially infinite number of classes which can be nested in one class instead of taking up an infinite number of variables in the global name space. Besides, a function can easily become a method :crazy: .
iseahound
Posts: 1446
Joined: 13 Aug 2016, 21:04
Contact:

Re: Extending a class without using the keyword "extends"

25 Aug 2018, 14:10

Fundamentally, constructors must be contained within objects, and destructors outside of objects. That's why I place all the pieces necessary for my constructor inside the class, as opposed to outside.
Helgef wrote:Besides, a function can easily become a method
But then the function becomes a method of a functor, and we have a higher order object, when a small object is fine by me.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Extending a class without using the keyword "extends"

25 Aug 2018, 14:36

Thats where the issues begin.
Imagine someone wants to give each library its own subdirectory.
If you use this function as part of 2 of your libraries this someone wont be able to use your libraries.
They will put library A (including the function) in folder A and library B (including the function) in folder B.
Also using a function will make it impossible to include your library inside another class.

Or simply put - avoid using functions in general - they are not an option with Autohotkeys current limited include system.
Recommends AHK Studio
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Extending a class without using the keyword "extends"

26 Aug 2018, 07:17

iseahound, I think you misunderstand me, I'm not trying to tell you that you should, or should not, do anything in any way or no way. I've made some general comments regarding the thread title, Extending a class without using the keyword "extends", I showed an alternate approach, and pointed out some limitations, maintainability being one.

nnnik, I think it is more managable to maintain one function (and its #include) than a potentially huge amount of two-line code snippets, scattered around various class definitions.
Or simply put - avoid using functions in general - they are not an option with Autohotkeys current limited include system.
I think you are limiting yourself more than Autothotkeys current include system does.

Cheers.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Extending a class without using the keyword "extends"

26 Aug 2018, 10:26

Taking in your response I didnt make my point clear enough - as you obviously did not comprehend it.
The library structures that I mentioned are not something completely rare or strange.
In fact I consider them to be a standard.
Since AutoHotkey does not have a proper include system, including libraries into your own classes as subclass is good practice.

I dont think there is anything limiting the current AutoHotkey include system - there is no include system at all.
There is a way to copy paste code from one file to another.
Calling that an include system would be going very far.
Recommends AHK Studio
iseahound
Posts: 1446
Joined: 13 Aug 2016, 21:04
Contact:

Re: Extending a class without using the keyword "extends"

26 Aug 2018, 11:14

I wasn't satisfied with the fact that it could be extended to a limit of one class, and taking into account some of your helpful suggestions and ideas here's an improved version.

Code: Select all

q := new uber.c()
MsgBox % q.test1
q.test2()
q.test3()

class uber {

   class a {
      test3() {
         MsgBox % "This is a method of class a."
      }
   }

   class b {

      init := this._init()
      _init() {
         this.base.base := (this.outer.a) ? new this.outer.a : new a
      }
      outer[] {
         get {
            if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
               Loop, Parse, _class, .
                  outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
            return outer
         }
      }

      test2() {
         MsgBox % "This is a method of class b."
      }
   }

   class c {

      init := this._init()
      _init() {
         this.base.base := (this.outer.b) ? new this.outer.b : new b
      }
      outer[] {
         get {
            if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
               Loop, Parse, _class, .
                  outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
            return outer
         }
      }

      test1 := "This is a property of class c."
   }
}
Top code is nested in a namespace (class) called "uber", while the Bottom code is free range.

Code: Select all

q := new c()
MsgBox % q.test1
q.test2()
q.test3()

class a {
   test3() {
      MsgBox % "This is a method of class a."
   }
}

class b {

   init := this._init()
   _init() {
      this.base.base := (this.outer.a) ? new this.outer.a : new a
   }
   outer[] {
      get {
         if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
            Loop, Parse, _class, .
               outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
         return outer
      }
   }

   test2() {
      MsgBox % "This is a method of class b."
   }
}

class c {

   init := this._init()
   _init() {
      this.base.base := (this.outer.b) ? new this.outer.b : new b
   }
   outer[] {
      get {
         if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
            Loop, Parse, _class, .
               outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
         return outer
      }
   }

   test1 := "This is a property of class c."
}
class c extends class b which extends class a.

Goals met:
- Avoid using __Init()
- maintainable
- v2 compatible (I think)
- doesn't pollute the global namespace (for some reason if you write hi := bye in a class "bye" will become a global variable)

EDIT: Simplifications
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Extending a class without using the keyword "extends"

27 Aug 2018, 07:30

I doubt this works as you intended. When you set an instance of b as base for c, the instance variables of that instance of b effectively becomes static variables, and every time you derive an object from c you reset these variables. Surely you meant for an instance of c to have the instance variables of b (and a) as its own instance variables, as if c extends b and b extends a? You should probably avoid using new, instead you need to set the chain of bases, and invoke __init() correctly. It will probably be fiddly, and you might need to reassign c/b/a.__init to some function/method which routes the calls appropriately. When you make new c(), you want c.__init() to call b.__init() which calls a.__init(), the calls should return in the order a before b before c. Ofc, depending on which details are important to you, you can disregard my comments as you see fit ;).
Structural example,

Code: Select all

; Chain the bases
c.base := b
b.base := a

; Save the original __init as o_init
c.o_init := c.__init 
b.o_init := b.__init 
a.o_init := a.__init 

; Reassign __init to router function
c.__init := func("f")
b.__init := func("f")
a.__init := func("f")

; test
z := new c
msgbox % z.aa "`n" z.bb "`n" z.cc "`n"  "`n"  z.s_aa "`n" z.s_bb "`n" z.s_cc "`n"  "`n" z.ma() "`n" z.mb() "`n" z.mc()

f(this){
	; 'this' is the new instance of 'c', assigned to 'z'
	local
	b := this.base							; 'b' is 'c' here (sorry)
	base_objects := [b]
	while b := b.base						; get all bases, 'b' becomes 'b' and then 'a'
		base_objects.push(b)
	while base_object := base_objects.pop()	; Call in "reverse" order
		base_object.o_init.call(this)		; calls a/b/c.o_init()
}


class a {
	static s_aa := 11
	aa := 1
	ma(){
		return a_thisfunc
	}
}
class b {
	static s_bb := 22
	bb := 2
	mb(){
		return a_thisfunc
	}
}
class c {
	static s_cc := 33
	cc := 3
	mc(){
		return a_thisfunc
	}
}

v2 compatible (I think)
Note that this.outer.b will cause a No object to invoke exception if this.outer doesn't result in an object.
doesn't pollute the global namespace (for some reason if you write hi := bye in a class "bye" will become a global variable)
Instance variables wrote:Expression can access other instance variables and methods via this, but all other variable references are assumed to be global.
Similar for static variables.

Cheers.
iseahound
Posts: 1446
Joined: 13 Aug 2016, 21:04
Contact:

Re: Extending a class without using the keyword "extends"

29 Aug 2018, 14:10

Helgef wrote:I doubt this works as you intended.
Cheers.
Final Version: Avoids the new keyword which was causing plenty of issues.

Code: Select all

q := new uber.c()
MsgBox % q.test1
q.test2()
q.test3()

MsgBox % q.__class
MsgBox % q.base.__class
MsgBox % q.base.base.__class
MsgBox % q.base.base.base.__class

class uber {

   class a {
      __New() {
         msgBox created
      }

      __delete() {
         msgbox deleted
      }

      test3() {
         MsgBox % "This is a method of class a."
      }
   }

   class b {
      static extends := "a"
      init := this._init(true)
      _init(endofunctor := "") {
         extends := this.extends
         under := ((this.outer)[extends])
            ? ((___ := (this.outer)[extends]._init()) ? ___ : (this.outer)[extends])
            : ((___ := %extends%._init()) ? ___ : %extends%)
         if (endofunctor)
            this.base.base := under
         else
            this.base := under
         return this
      }
      outer[] {
         get {
            if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
               Loop, Parse, _class, .
                  outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
            return outer
         }
      }

      test2() {
         MsgBox % "This is a method of class b."
      }
   }

   class c {
      static extends := "b"
      init := this._init(true)
      _init(endofunctor := "") {
         extends := this.extends
         under := ((this.outer)[extends])
            ? ((___ := (this.outer)[extends]._init()) ? ___ : (this.outer)[extends])
            : ((___ := %extends%._init()) ? ___ : %extends%)
         if (endofunctor)
            this.base.base := under
         else
            this.base := under
         return this
      }
      outer[] {
         get {
            if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
               Loop, Parse, _class, .
                  outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
            return outer
         }
      }

      test1 := "This is a property of class c."
   }
}

Breakdown:

Code: Select all


; Assume that class "big" is on the same level as "small"
class big {
   ; static variable to be declared once.
   static extends := "small"

   ; instance variable only instantiates when "new" is used.
   init := this._init(true)

   ; parameter = true means that this class has been instantiated.
   _init(endofunctor := "") {

      ; get the name of the class to extend
      extends := this.extends

      ; check if both classes are nested
      under := ((this.outer)[extends])

         ; check if they have a custom "_init" declaration. (NOTE: _Init, not __Init.)
         ? ((___ := (this.outer)[extends]._init()) ? ___ : (this.outer)[extends])
         ; Note that IsFunc() cannot be used, so a work around is to run the function directly, 
         ; save it to a temporary variable "___" and then check if ___ holds a value. 
         : ((___ := %extends%._init()) ? ___ : %extends%)

      ; class was instantiated with "new", set the base of the base. 
      if (endofunctor)
         this.base.base := under
      ; class was not instantiated, and base is empty and can be set
      else
         this.base := under
         
      ; returns this if called via another _init() function. 
      return this
   }
   
   ; see: https://autohotkey.com/boards/viewtopic.php?t=46828
   outer[] {
      get {
         if ((_class := RegExReplace(this.__class, "^(.*)\..*$", "$1")) != this.__class)
            Loop, Parse, _class, .
               outer := (A_Index=1) ? %A_LoopField% : outer[A_LoopField]
         return outer
      }
   }
}
It works.

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 234 guests