Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Class definition syntax for AutoHotkey


  • Please log in to reply
96 replies to this topic
Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
Currently the "base" pseudo-keyword is implemented in a way that requires the class to be looked up at run-time, so it behaves similarly to a double-deref. In v1, double-derefs can resolve to globals even if they're not declared and the function is assume-local. In v2, the variable must be declared (so you need to add global Class2) unless the function is assume-global. However, I intend to change this behaviour in one or more of the following ways, after I've released v1.1 and can get back to work on v2:
[*:2cu2xtu0]Have the "base" pseudo-keyword search only global variables. (This should probably be done in v1.1 since otherwise local variables can interfere.)
[*:2cu2xtu0]Implement an actual "base" keyword, which is resolved at load-time. My initial attempt was messy (i.e. hackish) and only gave a 20% approximate increase in performance for v1 scripts with #NoEnv.
[*:2cu2xtu0]Make all class definitions super-global. However, with the current implementation, global declarations would still be required in each function which precedes the class definition.

this.A := new A() ; this does absolutely nothing

This is the expected behaviour. If you use #Warn, you'll get this message:
Warning:  Using value of uninitialized variable.

Specifically: A  ([color=red]a local variable with same name as a global[/color])

        Line#
        005: obj := new B()
        010: {
        011: MsgBox,A.__New()
        012: Return,this
        013: }
        019: {
        020: MsgBox,B.__New()
--->    021: this.A := new A()  
        022: Return,this
        023: }
        025: Exit
        026: Exit
        026: Exit

It's perfectly valid to assign a class object to a local variable and use that with the new keyword. It doesn't even have to be a class object: as I mentioned earlier, new Foo(y) is equivalent to {base: Foo}.__New(y). (However, if __New isn't handled, the object is still returned.)

fincs
  • Moderators
  • 1662 posts
  • Last active:
  • Joined: 05 May 2007
As for the first issue: maybe I'm just being terribly ignorant, but why can't the base pseudo-keyword act like this.base.base, only that the callee's 'this' variable is set to the caller's?

As for the second: I find the behaviour counter-intuitive, as I would expect to be able to instantiate classes from inside functions. Maybe if the variable is blank it should search in the global scope?

And I've found a third: taking an object's address via the & operator seems to be broken (it returns the address of the var's internal buffer instead). Is this intentional? If so, then which commit changed it?

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

As for the first issue: maybe I'm just being terribly ignorant, but why can't the base pseudo-keyword act like this.base.base,

Consider this: If D is an instance of C and C extends B extends A, then D.base.base is B. That would only be the correct value for methods defined in C, not methods defined in B or A. base.F() needs to resolve to CURRENT_CLASS.base.F.(this). The script below demonstrates:
d := new C
d.F()

class A {
    F() {
        MsgBox % this.base.__Class " : " this.base.base.__Class " : " this.base.base.base.__Class
            . "`nA : " base.__Class
        base.F()
    }
}

class B extends A {
    F() {
        MsgBox % this.base.__Class " : " this.base.base.__Class " : " this.base.base.base.__Class
            . "`nB : " base.__Class
        base.F()
    }
}

class C extends B {
    F() {
        MsgBox % this.base.__Class " : " this.base.base.__Class " : " this.base.base.base.__Class
            . "`nC : " base.__Class
        base.F()
    }
}

as I would expect to be able to instantiate classes from inside functions.

You can. Just declare the class variable as global, either inside or outside the function. See also #3 in my previous post.

Maybe if the variable is blank it should search in the global scope?

In the expression new Class.NestedClass(), the new operator sees only the object returned by Class.NestedClass. It never sees the variable Class in that case. That is, the new operator accepts an object, not a class name.

it returns the address of the var's internal buffer instead

No, it actually returns the address of the internal Var structure, which is quite unintentional. I called TokenToObject(right) then ignored the result and used right.object (which overlaps with right.var via union). It's been broken since commit 275216e (pre alpha 1). :oops:

a4u
  • Guests
  • Last active:
  • Joined: --
Just a thought, but should __New return this by default - without having include the return, this?

fincs
  • Moderators
  • 1662 posts
  • Last active:
  • Joined: 05 May 2007
I see (and indeed, I was being terribly ignorant :lol:). So, I vote for implementing #1 and #3.

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007
Pretty amazing. Thank you :)

Just a thought, but should __New return this by default - without having include the return, this?

+1

Thread any User defined "return" as an error withhin an Constructor, and append automatically the "return, this" at the end of the Constructor.

__New(aFlavour) {
        
        this.Flavour := aFlavour
    }
"base.__New() ; Bar.__New.(this)" May be better solved in constructor redirection; eg:

    __New(aFlavour) : base() {
        this.Flavour := aFlavour
    }
It doesn't has to be in that C# style thougt, but calling an __New() Method by hand seems kind of ugly to me.

a4u
  • Guests
  • Last active:
  • Joined: --
I haven't been able to get __Delete to work when using __New. Also, though I see why it happens, it seems counterintuitive that the call metafunction would be triggered by __New, since I would think of it as a constructor:
obj := new Foo



class Foo {

    __New() {

        return, this

    }

    __Call(k) {

        MsgBox, 0, Call MetaFunction, Method = %k%

    }

    __Delete() {

        MsgBox, Delete

    }

}


Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

Just a thought, but should __New return this by default - without having include the return, this?

Done in v2a9.

Thread any User defined "return" as an error withhin an Constructor,

That would reduce flexibility without any real purpose. Also, a "constructor" at the moment is just an ordinary method. The only difference is who calls it.

May be better solved in constructor redirection;

Perhaps, but it's not a great benefit IMO and would require special handling for "constructors".

I haven't been able to get __Delete to work when using __New.

There was a reference-counting error. I've fixed it in v2a9.

Also, though I see why it happens, it seems counterintuitive that the call metafunction would be triggered by __New,

I was aware of that issue and have fixed it in v2a9. See the commit history for a full list of changes.

jethrow
  • Moderators
  • 2854 posts
  • Last active: May 17 2017 01:57 AM
  • Joined: 24 May 2009
Very impressive :D - the class syntax is turning out better than I imagined.

Thread any User defined "return" as an error withhin an Constructor,

That would reduce flexibility without any real purpose. Also, a "constructor" at the moment is just an ordinary method. The only difference is who calls it.

I definitely agree. Now, to get really picky: for the following, should __Delete ever be called? The object never exists outside of the class definition - which would never get past the constructor:
MsgBox, % Type(new Foo)

class Foo {
    __New() {
        return, "blank"
    }
    __Delete() {
        MsgBox, Delete
    }
}
... granted, I don't know why anyone would do this.

Also, the 32-bit download is pulling v2a4.
EDIT:

Clear your cache ...

:? I have that issue with Chrome/Iron often, but haven't had it with IE till now .... thanks.

Frankie
  • Members
  • 2930 posts
  • Last active: Feb 05 2015 02:49 PM
  • Joined: 02 Nov 2008
__Delete should always be called. If you are constructing a library class that does dllcall inits, it has to release the objects before the script exits. That's where I see the power in classes. The UI is simplified because they don't have to worry about all pitfalls if they are using a well designed class.
aboutscriptappsscripts
Request Video Tutorials Here or View Current Tutorials on YouTube
Any code ⇈ above ⇈ requires AutoHotkey_L to run

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

Also, the 32-bit download is pulling v2a4.

Clear your cache or try a different browser. Both links are aliases which are updated automatically when I release a new alpha.

aSEioT
  • Members
  • 87 posts
  • Last active:
  • Joined: 31 Oct 2010
I just try it, it's fantastic.

But I found if I define both __Call() and func() in the class, Then when I calling obj.Func(), It will first use the __Call() method and following with call the func(). Is that anticipated action or not!

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

Is that anticipated action

Yes. For each base object in the chain, __Call/Get/Set is invoked before looking for a method/field. It is designed this way so that the base object can have internal data which isn't necessarily inherited by derived objects.

Btw, read/write properties can be defined like so:
arr := new Array
MsgBox % arr.Length " / " arr.Capacity
arr.Insert("abc")
MsgBox % arr.Length " / " arr.Capacity
++arr.Capacity
MsgBox % arr.Length " / " arr.Capacity

class Properties {
    __Call(aTarget, aName, aParams*) {
        if IsObject(aTarget) && ObjHasKey(this, aName)
            return this[aName].(aTarget, aParams*)
    }
}

class Array {
    class __Get extends Properties {
        Length() {
            return (n := ObjMaxIndex(this)) ? n : 0
        }
        Capacity() {
            return ObjGetCapacity(this)
        }
    }
    class __Set extends Properties {
        Capacity(aNewCapacity) {
            return ObjSetCapacity(this, aNewCapacity)
        }
    }
}


maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
Just a suggestion: could ComObjConnect() also link to a class?

A bug / a question:
class simpleClass
	{
	__New(){
		this.pointer := 42
		}
	__Get(key){
		return this[key]
		}
	}

instance := new simpleClass
MsgBox % instance.pointer ; shows up correctly, with the correct value
MsgBox % instance.value ; this msgbox just never appears!
return
All in all, I really like the new class syntax :D (as you might have seen, I already released a class)

Regards
maul.esel
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

could ComObjConnect() also link to a class?

It could accept an object instead of a function name prefix, but there would be nothing "class" specific about it. I'll add it to the list.

A bug

No. What did you expect? this[key] doesn't exist, so it recurses into __Get (repeatedly, until you run out of stack memory and the program crashes).


I've decided to partially implement class var declarations in v1.1, to make defining static/class data easier. For example:
stk := new Stack
stk.Push("foo")
stk.Push("bar")
MsgBox % stk.Pop() stk.Pop()

; Without var declarations.
class Stack
{
    _StaticInit()
    {
        global Stack
        static _ := Stack._StaticInit()
        this.Push := Func("ObjInsert")
        this.Pop  := Func("ObjRemove")
    }
}

; With var declarations
class Stack
{
    var Push := Func("ObjInsert")
    var Pop  := Func("ObjRemove")
}
Although it might be convenient to be able to omit "this." for declared vars, I've decided against implementing it at this point. The main reason is that it wouldn't be possible to use them as output/input vars or in %derefs%. Furthermore, they couldn't be passed ByRef or have their &address taken.

I've (locally) changed the "new" operator to avoid invoking __Call when it is defined in the same class as __New. However, I've noticed that both __New and __Delete can cause a super-class __Call to be invoked. Calling base.__Get(k) or similar does the same. Calling base.__Call(k) first invokes __Call("__Call", k), then __Call(k). Although it's plausible that the super-class implements some of its methods via __Call, it seems more generally useful to bypass the meta-functions when using base.Something().