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
IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007
@Frankie: Neither :D

First off, a class/Object has Members. Either Fields/Properties or Methods. As AHK 1.1 is prototype based you are not forced to define class fields. You can create them at runtime. But I would not do that for reasons.

The Constructor should require necessary, important properties, that the class gets a valid inner status. Other Fields/Settings can be set after the class is instated.

mycar := new Car("Hubert")
mycar.Color := "Green"

class Car
{
	var Name
	var Color := "Black"
	
	__New(nom){
		this.Name := nom
	}
}

Thus - those are more patterns and design recommendations but you are free to make it as you like. It also very depends on the current issue which properties must be in the Constructor and which not.


I've done some example Script this day, you find two classes in it, which maybe get you to the idea how to design them:

#NoEnv 
#Warn
/*****************************************
DeviceManager Demo Script
by IsNull
******************************************
*/

deviceManager := new NetworkDeviceManager()
deviceManager.DeviceListChangedEvent := Func("OnDevicesChanged") ; Listen to the Event


; create a image list which holds our icons
imageListID1 := IL_Create(2)
IL_Add(imageListID1, "shell32.dll", 29) ; 17 printer ;16 computer
IL_Add(imageListID1, "shell32.dll", 132) 

/*
   Set up the GUI
*/
Gui +Resize
Gui, Add, ListView, xm r20 w700 vMyListView gMyListView, Name|ID|Type|Raum|IP
Gui, Add, Button, gAddDeviceEvent w200, [Add Random Device]
Gui, Add, Button, xp+300 gSimulateConFailEvent w200, [Simulate Connection failure]
LV_ModifyCol(2,0) ; hide ID Column
; Create an ImageList so that the ListView can display some icons:
LV_SetImageList(imageListID1) ; Attach the ImageList to the ListView so that it can later display the icons:

; Create a popup menu to be used as the context menu:
Menu, MyContextMenu, Add, Details, ShowDetailsCommand
Menu, MyContextMenu, Add, Ping, PingCommand
Menu, MyContextMenu, Default, Details  ; Make "Open" a bold font to indicate that double-click does the same thing.

Gui, Show,, Network Device Manager

Loop, 10
   Gosub, AddDeviceEvent

return



/*
   Event Handler for DeviceListChangedEvent
   Occurs when the Devices-List in the DeviceManager has changed
   We may want to update the Listview now:
*/
OnDevicesChanged(){
   global deviceManager
   
   LV_Delete() ; clear the ListView
   ; add all elements again
   for each, device in deviceManager.GetAllDevices()
   {
      stateIconNum := "Icon" ((device.IsOnline) ? 1 : 2)
      LV_Add( stateIconNum, device.Name, device.ID, device.Type, device.Raum, device.IP)
   }
   LV_ModifyCol() ; auto resize Cols
   LV_ModifyCol(2,0) ; hide ID Column
}

/*
   ShowCase Method - add a random device
*/
AddDeviceEvent:
   ; create a random device
   Random, num, 0, 999
   Random, ip1, 0, 9
   Random, ip2, 0, 9
   Random, ip3, 0, 9
   Random, typernd, 0, 1
   typestr := (typernd) ? "Computer" : "Drucker"
   rndDevice := new NetworkDevice(deviceManager, "Drucker " num, typestr, "10.12.1. " ip1 ip2 ip3 )

   deviceManager.AddDevice(rndDevice)
return

SimulateConFailEvent:
   for, each, dev in deviceManager.GetAllDevices()
   {
      Random, online, 0, 1
      dev.IsOnline := online
      dev.Update()
   }
return


/*
   Occurs when the User interacts with the LV
*/
MyListView:

   if(A_GuiEvent = "DoubleClick")  ; There are many other possible values the script can check.
   {
      Gosub, ShowDetailsCommand
   }
return

/*
   Occurs when the User request a Context-Menue
*/
GuiContextMenu:
   if (A_GuiControl != "MyListView")  ; Display the menu only for clicks inside the ListView.
      return
   
   focusedRowNumber := LV_GetNext(0, "F")  ; Find the focused row.
   if not FocusedRowNumber  ; No row is focused, dont show any context menue
      return
   
   ; Show the menu at the provided coordinates, A_GuiX and A_GuiY.  These should be used
   ; because they provide correct coordinates even if the user pressed the Apps key:
   Menu, MyContextMenu, Show, %A_GuiX%, %A_GuiY%
return

ShowDetailsCommand:  ; The user selected "Details" in the context menu.
   dev := DeviceFromFocusedRow()
   if(IsObject(dev)){
      dev.ShowDetails()
   }
return 

PingCommand:  ; The user selected "Ping" in the context menu.
   dev := DeviceFromFocusedRow()
   if(IsObject(dev)){
      dev.Ping()
   }
return


DeviceFromFocusedRow(){
   global
   focusedRowNumber := LV_GetNext(0, "F")  ; Find the focused row.
   LV_GetText(selectedID, focusedRowNumber, 2)
   dev := deviceManager.FindDeviceByID(selectedID) ; find the device Object with the given ID
   return dev
}


/*
   Occurs when the Winows is resized
*/
GuiSize:  
   if (A_EventInfo = 1)  ; The window has been minimized.  No action needed.
      return
   ; Otherwise, the window has been resized or maximized. Resize the ListView to match.
   GuiControl, Move, MyListView, % "W" . (A_GuiWidth - 20) . " H" . (A_GuiHeight - 40)
return

GuiClose:  ; When the window is closed, exit the script automatically:
ExitApp


/*
   This class represents a single NetWorkDevice like as a Printer/Computer
*/
class NetworkDevice
{
   var _devmanager
   var ID := -1 ;// -1 means no ID, let the devicemanager set a unique id
   var Name
   var IP
   var Type
   var IsOnline := true
   
   Ping(){
      MsgBox not implemented :-P
   }
   
   ShowDetails(){
      MsgBox % this.Name "{ IP:" this.IP ", Type: " this.Type ", Online: " this.IsOnline "}"
   }
   
   Update(){
      global
      row := this._devmanager.FindRowOfDevice(this)
      if(row != 0){
         stateIcon := "Icon" . ((this.IsOnline) ? 1 : 2)
         LV_Modify(row, stateIcon, this.Name, this.ID, this.Type, this.Raum, this.IP)
      }
   }
   
   __New(devmanager, name, type, ip){
      this._devmanager := devmanager
      this.Name := name
      this.IP := ip
      this.Type := type
   }
}

/*
   This Class Represents the DeviceManager
*/
class NetworkDeviceManager
{
   var _devices := [] ; list which holds all Devices
   var _currentID := 0
   var DeviceListChangedEvent := 0
   
   
   AddDevice(device){
      if(device.ID == -1)
         device.ID := this._currentID++
      this._devices.Insert(device)
      this.OnDeviceListChanged()
   }
   
   RemoveDevice(device){
      i := this.IndexOf(device)
      if(i != 0){
         this._devices.Remove(i)
         this.OnDeviceListChanged()
      }
   }
   
   GetAllDevices(){
      return this._devices
   }
   
   IndexOf(device){
      for index, dev in this._devices
      {
         if(dev.ID == device.ID)
            return index
      }
      return 0
   }
   
   Contains(device){
      for each, dev in this._devices
      {
         if(device.ID == dev.ID)
            return true
      }
      return false
   }
   
   FindDeviceByID(id){
      for each, device in this._devices
         if(device.ID == id)
            return device
   }
   
   FindRowOfDevice(device){
      
      Loop, % LV_GetCount()
      {
         LV_GetText(id, a_index, 2)
         if(id == device.ID)
            return A_index
      }
      return 0
   }
   
   /*
      Occurs when the DeviceList changes
   */
   OnDeviceListChanged(){
      this.DeviceListChangedEvent.() ;// call the EventHandler
   }
}


guest3456
  • Members
  • 1704 posts
  • Last active: Nov 19 2015 11:58 AM
  • Joined: 10 Mar 2011
since we must use 'var' to declare class variables inside a class, perhaps force the use of 'function' (or 'method') to declare class methods?

class foo
{
  var myClassVar

  function myClassMethod() {
    ;
  }
}

then there are three special keywords related to class definitions: class, var, function, each for its specific purpose. purely for aesthetics though, to differentiate between a class method function and a normal function. or not worth it?

Frankie
  • Members
  • 2930 posts
  • Last active: Feb 05 2015 02:49 PM
  • Joined: 02 Nov 2008
Blah, extra typing. The use of function is only useful when it returns some pointer to that function (e.g. Javascript and Lua).
aboutscriptappsscripts
Request Video Tutorials Here or View Current Tutorials on YouTube
Any code ⇈ above ⇈ requires AutoHotkey_L to run

a4u
  • Guests
  • Last active:
  • Joined: --
class C {
	__New() {
		; this.base := this.base.Clone()
	}
}

so if i uncomment out your constructor, then this object would behave like a normal "class based" oop class. but if we leave the constructor commented, then it behaves like a prototype, which can be modified on the fly and all instances get updated as well. is this right?

It might be OK to understand it like that, but no. This is why I suggested using the keyword prototype rather than class, since there seems to be confusion. Not that I'm an expert, but a class in a blueprint, not an actual object. A prototype is an object which you base other objects on. Using the code above, when you call new C, a new object is created where the base is the object (prototype) C. If you un-comment the constructor, it assigns a Copy of the Prototype Object C as the base, rather than C itself. This makes each created object unique, rather than having the same base (at the expense of memory/performance).

:idea: I could see this being the desired behavior for many users. Even though the code is simply one line, the understanding of what is happening may be a stumbling block. I might suggest creating a keyword that can be called in the class definition that will make the new constructor use a Clone of the prototype, rather than the prototype itself.

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007

I could see this being the desired behavior for many users. Even though the code is simply one line, the understanding of what is happening may be a stumbling block. I might suggest creating a keyword that can be called in the class definition that will make the new constructor use a Clone of the prototype, rather than the prototype itself.

+1
That sounds very good and removes some of the (unexpected) prototype beahviour when dealing with classes in AHK 1.1/v2.

Thus, I'm not sure if it should be even necessary to mark the class, as this should be the default behaviour. The other way around - if someone want to define a Prototype-behaviour object, a specail mark/keyword could be used (to get the current beaviour):

eg.
prototype class Car
{

}

I'm also unable to see any benefit from the function keyword.

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

This makes each created object unique, rather than having the same base

Each created object is unique, though it may have the same base as some other object. (That's the whole point of having a "base" mechanism.)

I might suggest creating a keyword that can be called in the class definition that will make the new constructor use a Clone of the prototype, rather than the prototype itself.

What's the point? You may as well copy the properties of the base object into "this", or return the clone of the base object and just discard "this". Both options are more memory-efficient than your suggestion, but almost as pointless.

That sounds very good and removes some of the (unexpected) prototype beahviour

Like what?

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007
myfoo := new Foo()
myfoo.Base ;--> should refer to Bar instead of Foo, as myFoo is the Foo itself

class Bar
{
}

class Foo : Bar
{
}

Lexikos, I see your point, as Bar and Foo aren't "Types", they are Objects itself -> which is actually not a problem, but the Handling this way is somewhat confusing.

It's also not that easy to resolve, as otherwise you would lost the Prototype-Based behaviour. Maybe there should be some specail Properties on created class instances to acces the type of it (currently the "base") and also access the Parent Type directly, if any.

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

myfoo.Base ;--> should refer to Bar instead of Foo, as myFoo is the Foo itself

That doesn't make any sense. myfoo is an instance of Foo, or an object derived from Foo. It is not Foo itself, and I don't understand why you would see it as such. If you consider the base object a "class", myfoo.base returns the class of myfoo, equivalent to (pseudo-code) myfoo.class or typeof(myfoo). I don't see how the parent type is even relevant, given an instance of a derived type.

guest3456
  • Members
  • 1704 posts
  • Last active: Nov 19 2015 11:58 AM
  • Joined: 10 Mar 2011

myfoo := new Foo()
myfoo.Base ;--> should refer to Bar instead of Foo, as myFoo is the Foo itself

class Bar
{
}

class Foo : Bar
{
} 

That doesn't make any sense. myfoo is an instance of Foo, or an object derived from Foo. It is not Foo itself, and I don't understand why you would see it as such.


this is the problem that a4u was alluding to, ccomparing normal class-based oop with prototype-based.

isNull is referring to the Foo "class" as if it were a blueprint, but in actuality, the Foo object is created, and then myFoo is a second object that is created with base Foo.

so, as i understand it, the code above actually translates into something like this, with three objects created

bar := Object()                   ;// class bar {}
foo := Object("base", "bar")     ;// class foo extends bar {}
myfoo := Object("base", "foo")   ;// myfoo := new foo()

however people coming from class-based oop would incorrectly assume that only one object "myfoo" is created, while the two "classes" are blueprints as a4u mentioned.



now, as for the cloning in the constructor

class C {
	__New() {
		; this.base := this.base.Clone()
	}
}

Using the code above, when you call new C, a new object is created where the base is the object (prototype) C. If you un-comment the constructor, it assigns a Copy of the Prototype Object C as the base, rather than C itself.


this would translate into something like:

c := object()                        ;// class c
cfoo := object("base", c.clone())    ;// cfoo := new c()

;// now any modifications to c won't affect cfoo
c.key := value                       ;// cfoo.key != value


fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
While we're at this topic, were there technical reasons why you decided to use the base mechanism instead of creating a (shallow?) copy? Or was it a design decision?

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

What's the point? ... return the clone of the base object and just discard "this".

Thank You :) . The point is I had a critical misunderstanding. In this case, there is no reason to even use new, the user could just call obj := C.Clone() directly.

@fragman - I'm quite sure this is a design decision. First, it is more more memory-efficient. Second, it allows you to modify the behavior of every object of a given class. As stated in the previous paragraph, if you want a copy of the prototype, just create a shallow copy.

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
It's not a problem at all for me, I was just wondering about the motivation behind it.

I have a few questions that are not clear to me yet:

1)Reference counting:
If I never use &obj, do I need to worry that the __Delete destructor might not get called when I delete all variables holding references to the object?
(Excluding local variables and program exit like mentioned in the docs)

2)May I have class methods and global functions of the same name?

3)Is something like this valid?
class x
{
var z := "foo"
__New(z)
{
this.z := z ;I assume this is valid?
msgbox % z ;Do I need to use this.z here?
}
}

I also wanted to thank you again for implementing class syntax. This makes more structured OOP programming easier to do and is already helping me very much.

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

I also wanted to thank you again for implementing class syntax.

+1

As far as your questions, did you test any of these?

1) Unless you manually change the reference count, when all references to an object are deleted, the __Delete destructor is called.
2) Yes, considering the function name would be obj.func()
3) The key *z* will exist in the new object, and in the base, as 2 separate values:
obj := new x("bar")
MsgBox, % "obj Enum:`n" obj.enum() "`n`nx (obj.Base) Enum:`n" x.enum()

class x {
	var z := "foo"
	__New(z) {
		this.z := z ;I assume this is valid?
	}
	enum() {
		for k,v in this
			t .= "   " k " = " (IsFunc(v)? "*Func":v) "`n"
		return, SubStr(t,1,-1)
	}
}


guest3456
  • Members
  • 1704 posts
  • Last active: Nov 19 2015 11:58 AM
  • Joined: 10 Mar 2011
a4u, wow thanks so much for that example code :) ill probably use that enum() as a standard method for all my objects just so i can debug them and see whats going on

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

however people coming from class-based oop would incorrectly assume that only one object "myfoo" is created, while the two "classes" are blueprints as a4u mentioned.

Is it not common in OOP to think of everything as an object? I know at least one class-based scripting language which exposes classes as objects. Additionally, .NET exposes objects representing classes, their methods, properties, etc. It seems likely that classes are objects in many languages, at least during compilation.

Anyway, why should anyone care whether the "blueprint" itself (or "rough sketch", if you prefer) is an object or something more abstract? If I were to make the class objects inaccessible by script (so that as far as you know, it isn't an object) and restrict which keys the derived objects could use to just the ones declared in classes (so that classes are strict blueprints), would that somehow make the whole concept easier to grasp? I don't think so. I think you're overcomplicating things.

While we're at this topic, were there technical reasons why you decided to use the base mechanism instead of creating a (shallow?) copy? Or was it a design decision?

The premise of your question seems to be that such a decision was consciously made. I never seriously considered cloning as an option (and I'm slightly curious about why anyone would). Keep in mind that the base mechanism was designed back in 2009, and the class definition syntax was intended purely as syntax sugar around the existing design. That said, actual run-time inheritence seems much more logical to me than "inheritence" by cloning, and is also more flexible.