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
jethrow
  • Moderators
  • 2854 posts
  • Last active: May 17 2017 01:57 AM
  • Joined: 24 May 2009

If the feasibility of more advanced features improves at some point, compatibility can be broken with another major version change.

Since realizing the approaches used in other prototype-based languages aren't necessarily feasible for AutoHotkey, I've overcome my initial resistence to the idea of adding class-definition syntax.

Just to clarify, do you mean that you may move back away from class-definition syntax to a prototype-based syntax if advanced features (such as closures and anonymous functions) improve?

Also, if we are going to have class-definition syntax, will the current objects become inextensible - and just be normal associative arrays?

LiquidGravity
  • Members
  • 156 posts
  • Last active: Oct 11 2014 04:11 PM
  • Joined: 26 Jan 2009

MyCar as new Car() ; Make a new Car

Where did you get "as" from? Use an assignment.

Sorry, It was more-less sudo code and you are right.

OOP lives form data encapsulation so control access modifiers is definitely important.

You can just use a unique prefix for private members and discourage users from accessing them directly. If they go ahead and do it anyway, that's their problem.

What would then be any different from just using a prefix and forget classes entirely? No offense intended, it just doesn't make sense to me. The reason I bring up this type of private accessibility is because I could see it being very beneficial - specifically for use in libs.

Also, what determines the name for the constructor? Can we have deconstructors? The latter would be helpful for class wrappers of DLLs that need to release or do similar actions before exiting the script. Deconstructors should also be called when an class object is assigned to another value, even if that value is of the same class type.
foo := new bar() ; Call constructor
foo := "some other value" ; Call deconstructor

I'd love to just do the following to clean up and get rid of variable all together (including any sub functions and variables) if possible.
Kill(foo)


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

For large classes, would support across multiple files be possible?

Without any explicit support, this would be possible:
class Foo
{
    #Include Bar.ahk
    #Include Baz.ahk
}
I've also considered:
[*:2dlkkj93]Merge classes with an explicit keyword, such as C#'s partial class, or something more original, like "class Foo continued".
[*:2dlkkj93]Merge classes implicitly if they have the same name. However, it might be more intuitive to detect "duplicate class definitions" as errors.As I mentioned in my example, "class Foo" would actually introduce a variable "Foo" and store a prototype object inside it. (When I say "prototype object", I mean an ordinary object which will be used as the base/prototype of another object.) Each var or method declaration would store a value (the var's initial value or a reference to a function) in the object. (Actually, I mentioned that in the example.) Thus, classes wouldn't be "merged" so much as the second "class Foo" would simply resolve to the same variable and continue to store values in the original prototype object.

Also, what determines the name for the constructor?

In my example, I used __New, for consistency with the existing meta-functions __Get, __Set, __Call and __Delete.

Can we have deconstructors?

That's what __Delete is.

Just to clarify, do you mean that you may move back away from class-definition syntax to a prototype-based syntax ...

No, they would co-exist. Technically, there isn't any syntax specific to prototype functionality. It's convenient to say "class definition", but it's actually a prototype definition, and is purely syntax sugar. I probably wouldn't be considering them at all if they didn't fit into the current design. Closures and anonymous functions provide additional purpose beyond just making prototype-based designs easier to use.

Also, if we are going to have class-definition syntax, will the current objects become inextensible - and just be normal associative arrays?

No. See above. Something like x := new y() would be equivalent to x := {base: y}, x.__New().

What would then be any different from just using a prefix and forget classes entirely?

Functionally, no (read: syntax sugar), but some users seem to think it's more readable and/or intuitive. There have already been two posts in this thread to that obvious effect. Anyway, I have yet to see a compelling reason to implement access controls. What reason would users have to attempt to access an object's private data fields, and what reason do you have to try to stop them if they do? If they believe they have a valid reason to do it, they can always just remove the access controls from your script.

I'd love to just do the following to clean up and get rid of variable all together (including any sub functions and variables) if possible.

Kill(foo)

Why? Also, what do you mean by "sub functions"? (Methods are defined in the prototype object bar, not in the derived object foo. Function references stored directly in the object would be released when the object is freed. The object is freed when all references to it are removed, such as by foo := "".)

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
I believe that access keywords are only used in combination with get/set functions. But as __Get and __Set are already available I agree that there is not much point to them. The only possible use would be for "marking" variables which aren't supposed to be accessed from outside of the class.

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
Very interesting topic :)
I do not heavily use objects for now, but maybe you can change this ;-)

Although it might be a bit off-topic:
If you're introducing this support for classes, it would be a (good) idea to support similar for structs.
The only thing is to difference between a class and a struct (when using both with new),
but I'm sure you'll find a solution. :D

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

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
I don't think this is necessary. Structs mostly differ by not supporting functions AFAIK. Since AHK classes would translate to objects internally, the result would be the same for structs. This means that you can just use a class and the outcome will be the same.

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
Well, I'm not sure but I think they differ a lot more:
I could not use this "struct"-object e.g. in a DllCall(). Currently you have to use NumPut() and NumGet(). It would be a lot easier like this:
/* old code:
VarSetcapacity(STRUCT, 16, 0)
Loop 4
    NumPut(member%A_Index%, STRUCT, 0)
*/
; new code:
_struct := new MyStruct
_struct.member1 := true
_struct.member2 := 42

; for both:
DllCall("MyStructDllCall", "UINT", &_struct)
This would require something like
struct MyStruct
   {
   uint member1
   uint member2
   int member3
   int member4 = 42 ; maybe an optional default value?
   }
(I think it's similar in C etc.)
Anyway, that's just a suggestion, and off-topic, too.

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

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
Compatibility to C/C++ structs is another thing. Have you tried out the struct lib?

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
Planned syntax for v1.1 - almost complete:
; Equivalent to Foo := {__Class: "Foo", base: Bar}, except that it is done
; at the point the class definition is parsed and Bar must already exist.
class Foo extends Bar
{
    ; Class variables will not be supported in v1.1, but may be in v2.x:
    ; var Flavour := ""
    
    ; Class definitions can contain nested class definitions, up to 5 deep.
    ; Equivalent to Foo.NestedClass := {__Class: "Foo.NestedClass"}.
    class NestedClass {
        ; Method definition.  See below.
        Method() {
            ...
        }
    }
    
    ; Method definition.  Assigns a function reference to Foo.Taste.
    Taste() {
        ; All methods have an implicit "this" parameter, which contains the
        ; target of this method-call.  If Foo.Taste() is called directly,
        ; "this" contains a refrence to Foo.  Thus, each method can be used
        ; as both an instance method and a static method.
        if this.Flavour
            return this.Flavour

        ; If "base" is followed by "." or "[", it invokes the super-class
        ; of this class -- Bar -- but implicitly passes "this" instead of
        ; the class object.  It is equivalent to Bar.Taste.(this), except
        ; that "global Bar" is not needed.
        ; For static method-calls such as Foo.Taste(), the following call
        ; will pass Bar.Taste a reference to the current class (which may
        ; be Foo or a derived class).
        return base.Taste()
    }
    
    ; The following are equivalent:
    ;    x := new Foo(y)
    ;    x := {base: Foo}.__New(y)
    __New(aFlavour) {
        base.__New()  ; Bar.__New.(this)
        this.Flavour := aFlavour
        return this
    }
}
I have more to say but have run out of time. Any questions?

jethrow
  • Moderators
  • 2854 posts
  • Last active: May 17 2017 01:57 AM
  • Joined: 24 May 2009
Without using the new class syntax, will the this parameter be accessible when assigning a function to an object key? Or will the user still have to assign the function in the base in order to make it a method?
obj := {foo:"bar", Msg:"Msg"}
obj.Msg()

Msg() {
	MsgBox, % this.foo
}
... also, I'm rather impressed at how quickly you implemented the new syntax :)

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
The current behaviour will most likely be retained for v1.x. However, it actually doesn't apply to "function objects". For example:
obj := {foo: "bar", Msg: Func("Msg")}
obj.Msg()
obj.Msg.(obj)  ; This also works if obj.Msg contains a function name.

Msg(this) {
   MsgBox, % this.foo
}

Func(func, this=0, prms*) {
    if IsObject(func)   ; __Call
        return func.(this, prms*)
    static base := {__Call: "Func"}
    return {"": func, base: base}
}
When you call obj.Msg(), it's basically equivalent to (obj.Msg)[obj](). Ordinarily (obj.Msg)[obj] doesn't exist, so it's roughly equivalent to obj.Msg.base.__Call(obj.Msg, obj).

Class definitions use function references. That is, built-in and user-defined functions are now also objects (although in my current build only class methods are accessible to the script). They behave much the same as the Func object defined above:
[*:3hvfu1b9]func[obj](prms*) runs the script function with obj as its first parameter. As a consequence, if func is stored in obj.method (or obj.base.method), calling obj.method() passes obj as the first parameter. If this is undesired, the syntax below can be used.

[*:3hvfu1b9]func.(prms*) or func[""](prms*) includes only prms* in the parameter list. (The script above has this behaviour because func[""] directly contains a function name.) If func is a function name instead of an object, func.(prms*) also acts this way in v1.0.95+.Also note that "this" is not a keyword, but a parameter which is added automatically for any function defined inside a class definition. As such, it must be explicitly declared for ordinary functions like in the example above. It can also be reassigned by the script. On the other hand "base" is a pseudo-keyword which will only work if followed by "." or "[", and only if the script hasn't assigned a value to "base". This behaviour might change in v2.x.

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
Since class definitions will automatically use function references, it would seem unfair not to provide some way to access them for other uses. I think a function is probably the only good option, but I'm not sure about the name. What do you think about the following?
[*:2jb5cvo7]FindFunc
[*:2jb5cvo7]GetFunc
[*:2jb5cvo7]Func

[*:2jb5cvo7]IsFunc - might be a little weird, and would break scripts which use the returned param count. (I mention it because before IsFunc was implemented, I considered having it return a pointer to the Func structure. Also, IsFunc would be mostly obsolete if FindFunc/GetFunc/Func was implemented, since the min/max params and other information could be retrieved via the Func object.)

a4u
  • Guests
  • Last active:
  • Joined: --

I think a function is probably the only good option ...

Could you provide an example of what you mean? If I'm understanding correctly, that is what I was trying to get at when talking about accessing local labels outside of a function:

... what do we think about allowing local labels to be addressed from anywhere by prefixing the function name ...

... This would be particularly nice if it applies to a Class [function] definition ...

; ClassName:function()

Foo:Taste()


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

Could you provide an example of what you mean?

It's basically the same as Func() in my previous example: {Msg: Func("Msg")} should store a pre-resolved function reference instead of a function name. This would improve performance and ensure the function always receives a value for the "this" parameter, even if it is called directly via the class object.

Foo:Taste()

The class definition in my previous example can be constructed at run-time (but it would only be equivalent if Func() was built-in):
; class Foo extends Bar
Foo := { base: Bar }

    ; class NestedClass
    Foo.NestedClass := { }
        ; Method()
        Foo.NestedClass.Method := Func("Foo_NestedClass_Method")
        Foo_NestedClass_Method(this) {
            ;...
        }
    
    ; Taste()
    Foo.Taste := Func("Foo_Taste")
    Foo_Taste(this) {
        if this.Flavour
            return this.Flavour
        ; base.Taste()
        global Bar
        return Bar.Taste.(this)
    }
    
    ; __New()
    Foo.__New := Func("Foo___New")
    Foo___New(this, aFlavour) {
        ; base.__New()
        global Bar
        Bar.__New.(this)
        this.Flavour := aFlavour
        return this
    }
This should make it clear that Foo.Taste() is already valid as a natural consequence of the way objects work. Func() is necessary because storing a function name is not equivalent to storing a function reference, as I explained previously.

fincs
  • Moderators
  • 1662 posts
  • Last active:
  • Joined: 05 May 2007
I can't seem to get this working with v2.0-a008-a5f1174, any ideas?
obj := new Class2()
obj.Method()

class Class1
{
	Method()
	{
		MsgBox, A
	}
}

class Class2 extends Class1
{
	Method()
	{
		base.Method() ; this doesn't work?
		MsgBox, B
	}
}

EDIT: More problems:
obj := new B()

class A
{
	__New()
	{
		MsgBox, A.__New()
		return this
	}
}

class B
{
	__New()
	{
		MsgBox, B.__New()
		this.A := new A() ; this does absolutely nothing
		return this
	}
}