First thoughts after coming back from a long hiatus

Discuss the future of the AutoHotkey language
User avatar
fincs
Posts: 527
Joined: 30 Sep 2013, 14:17
Location: Seville, Spain
Contact:

First thoughts after coming back from a long hiatus

15 Oct 2019, 19:25

As some of you may have noticed, I haven't been here in quite a while (although I do lurk from time to time). However, several recent AutoHotkey v2 releases have been increasingly catching my eye, and tempting me with the idea of coming back. After all, I have unfinished business and SciTE4AutoHotkey is unfortunately rotting away without receiving an update in years :oops:

As a result I've been binge reading some of the new v2 documentation for the past days or so. I have to say, I am really impressed with how clean and straightforward v2 syntax looks now, how many of the old quirks are gone, how so many things are much better integrated with objects, and how built-in functionality no longer does clunky or unexpected things (for the most part I guess). I'm actually tempted to have fun with AHK scripting again, which is something I haven't felt in a long time. There are a couple of things that have come to mind about the current state of v2 though, which I hope to express here:

Syntax
  • Now that the old command syntax is gone (finally!) and AutoHotkey is now able to detect line continuations by enclosure, continuation sections feel really out of place. The main usage for them has always been multiline strings and they're basically a preprocessor stage with no intrinsic semantic value at all. They can sometimes get in the way of expression syntax (thankfully alleviated by the ) rule), and they have terse and hard to read options that can change basic language rules such as escape characters. I feel like there should be an actual syntax designed explicitly for multiline strings (think of Python's """ or C++'s raw string literals), and it would supersede continuation sections in v2. On top of that, continuation sections make it considerably difficult to write an accurate syntax highlighting parser/lexer for the v2 language (unless we cheat a bit and treat all continuation sections as strings, like the old Scintilla AHK1 lexer did).
  • I'm glad to see my old proposal for @Func function-reference syntax still being considered.
  • Closures, missing parameters, and fat arrow functions rock. I can't wait to write scripts using them.
  • At some point there was a string variable interpolation syntax, but that was removed. Presumably this was due to the fact that Format is superior and should be used instead. However some popular languages do have variable interpolation as a way to cut down on syntactic noise stemming from repeated concat operators. If variable interpolation in strings is reintroduced then we can get rid of auto-concat, which can be seen as kludgy syntax that results in unexpected/unintuitive results (Function(Param) works, Function (Param) does not).
  • Some #Defines seem to still accept parameters resembling the old command syntax, most notably #Include. I think the latter needs some kind of redesign, probably to make it more useful in its goal of allowing the usage of external libraries written in script (and some kind of solution needs to be found for libraries providing classes).
Objects
  • Overall I like the new design, the type hierarchy is much simpler, the object invocation protocol is more straightforward and with less caveats. The __Item property handling obj[...] syntax is genious.
  • Building on the above, I think it should be more or less possible to add methods or properties for other operators (i.e. + -> __Add(rhs) and so on) or implicit conversions (i.e. __String, __Integer, ...). Current design is flexible enough that this can be left for post-v2 though.
  • __Set is currently (this, Name, Params*, Value). Since only the last parameter can be variadic and the Value of a set operation is supposed to be mandatory, I think for consistency and ease of use it should be (this, Name, Value, Params*).
  • Base vs Prototype can be a bit confusing and it's taking me some time to understand this stuff. I know the former is the base object we all know and love from v1 which is used during object operations, and I can understand that the base object used in all instances of a class is called Prototype and it is stored as the Prototype property of a class (however the object for the class itself is in fact just an instance of Class so its base ought to be Class.Prototype, right?). There's some potentially confusing wording in the Class page: "Object is based on Class.Prototype, which is based on Object.Prototype" (at first this read to me like "Object inherits from Class which inherits from Object"...), and it can also seemingly appear to be contradicted by this bit in the Object page: "All instances of Object are based on Object.Prototype, which is based on the Any prototype", but of course the object that represents a certain class is a separate thing from the prototype used as the instances' base; which makes sense. I'm not sure what happens in subclasses though (i.e. class Sub extends Main). If I had to guess, I'd assume that Sub.Base == Main and Sub.Prototype.Base == Main.Prototype and Main.Base == Class.Prototype because the documentation seems to imply that you can access static members (fields/methods) of a class from any of its subclasses.
  • Despite the Array/Map separation, there is a base Object type which... is essentially two associative arrays put together (one for "own properties" and "own methods"). From what I understand, this base Object type is the backbone of all objects (assuming non-COM of course), and it is used to construct Prototype objects for the various classes, as well as the class objects themselves. However (and this is the confusing part) there is still an Object(...) construction function which takes key/value pairs (which then become "own properties"), and the { ... } syntax still constructs these prototype objects. This caught me by surprise (and I had to check AHK source code to verify), I expected { ... } to create Map objects just like [ ... ] creates Array objects. Plain objects that are neither Map nor Array still seem to accept obj.property (:= value) syntax for modifying or retrieving arbitrary fields, just like an associative array. Dynamic properties (i.e. get/set functions in disguise) are treated specially though (i.e. defined with DefineProp). I suspect this was probably kept in order to have a convenient mechanism to support class fields (both static and per-instance), however it feels kind of confusing/redundant and/or defeating the point of separating Array/Map into different types.
Library & Misc
  • There's a separate ClipboardAll type, however it looks to me like a holdover from before Buffer was introduced. I think ClipboardAll() should just return a Buffer.
  • I know AHK objects implement IDispatch so they can be passed as COM objects. I can't remember where to find this in the documentation though. In general, COM marshalling rules are a bit hard to find information on.
  • I really like the enhancements made to DllCall (including the new ComCall function) that integrate so much better with objects, and the new HRESULT handling, and how you can easily wrap stuff using bound functions. However, I don't like how ComObjCreate still returns a raw IUnknown pointer instead of a ComObject of type 0xD for non-IDispatch objects.
  • ComObjError should probably not exist (i.e. it should always be enabled) since it makes writing robust wrappers slightly more cumbersome.
  • NumPut's new mode is nice and with a bit of wrapping/bound-function magic it could be used to serialize structs in one go; however in practice it might not be as useful due to it not obeying struct packing rules (basically, all struct fields are aligned to the required alignment of their type, which is often just the size of the type itself). An alternative solution would be to use explicit padding fields in struct wrappers, but it might not be a great solution.
  • I still think Options should not be the first parameter for GuiCreate or Gui.AddCtrl. Speaking of which, Gui.Add("Ctrl", ...) vs Gui.AddCtrl(...). One of these definitely has to go, and I'm not sure which.
  • There's still three different types of Tab controls. There's got to be a way of bringing them together into a single one.
  • Pipes/delimiters in Guis have to go in favour of real arrays.
  • IsSet now exists. I'm not sure if this is already the case, but for optional function parameters accepting empty string as default, it would be useful if IsSet would return true if the parameter is not passed. This would make it trivial to replicate the same kind of non-constant-default-value behaviour observed in built-in functions (i.e. if !IsSet(param), param := GetSomeDynamicValue()).
  • I think this is already planned anyway, but VarSetCapacity and the special variable-as-buffer modes of NumPut/Get/file ops definitely need to be removed. I think we're no longer in a position where we need to care about micromanaging variables' internal string buffer; and for binary data there's the Buffer object which is superior and more convenient in every way.
Overall, the AHK v2 of today is nothing like the early alphas I last had a chance of playing around with; and the language has matured quite a lot. There are still a few rough edges, but we're already getting there. I'm very looking forward to seeing v2 finally finished and rolled out. I've always been proud of being a loyal AutoHotkey user. AutoHotkey has always been my home scripting language (above Python or JS or others), and I'm sure v2 will once again bring our dear little language back in the much deserved spotlight.
fincs
Windows 11 Pro (Version 22H2) | AMD Ryzen 7 3700X with 32 GB of RAM | AutoHotkey v2.0.0 + v1.1.36.02
Get SciTE4AutoHotkey v3.1.0 - [My project list]
guest3456
Posts: 3454
Joined: 09 Oct 2013, 10:31

Re: First thoughts after coming back from a long hiatus

15 Oct 2019, 21:46

fincs wrote:
15 Oct 2019, 19:25
I feel like there should be an actual syntax designed explicitly for multiline strings (think of Python's """ or C++'s raw string literals), and it would supersede continuation sections in v2.
+1
fincs wrote:
15 Oct 2019, 19:25
At some point there was a string variable interpolation syntax, but that was removed.. If variable interpolation in strings is reintroduced then we can get rid of auto-concat, which can be seen as kludgy syntax
it was really nice. it was more than variable interpolation, it was full expression interpolation. a perfect feature for a scripting language imo. both of these used to work in earlier v2 builds:

Code: Select all

MsgBox("Hello %username%!")
MsgBox("two plus two equals %2+2%")
def in favor of re-introducing that, and also removing auto concat. i hate auto concat

in general i still feel like v2 has suffered from scope creep, but i understand lexikos' reasonings. glad to see you back though

User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: First thoughts after coming back from a long hiatus

16 Oct 2019, 15:19

- Nice to see you back fincs.

- Re. multi-line strings:
- I don't want, but wouldn't oppose an additional syntax *alongside* continuation sections, but what exactly? Languages tend to do multi-line strings differently.
- I've generally found that multi-line strings is something that programming languages do badly.
- The C++/Python syntaxes are obscure/quirky, un-memorable, and a bit cryptic like RegEx.
- Whereas I find that I can reproduce the AHK continuation section syntax from memory, and that it's elegant by comparison, and generally flexible re. storing text verbatim.

Code: Select all

#Python
var = r"""!"#$%&'()*+,-./0123456789:;<=>?
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
`abcdefghijklmnopqrstuvwxyz{|}~"""

//C++
var = R"***(!"#$%&'()*+,-./0123456789:;<=>?
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
`abcdefghijklmnopqrstuvwxyz{|}~)***";

;AutoHotkey
var := " ;continuation section (AHK v2)
(`
!"#$%&'()*+,-./0123456789:;<=>?
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
`abcdefghijklmnopqrstuvwxyz{|}~
)"
- Re. interpolation syntax:
- I prefer that anything within quotes *is a string*. Consistency.
- I prefer that MyFunc (Args*), with a space in-between, be disallowed. Consistency.
- Concatenation is *so* common, it's better to have auto-concat, than a concat operator everywhere. Practicality.
- (Since people can explicitly use the concat operator, ., if they want to, I don't know why anyone would oppose auto-concat. If you really wanted it, you could ask for a #Warn mode to always require a concat operator.)
- (I don't use the concat operator, apart from for line continuation. I like the general requirement of an operator, to continue an expression to the next line, it makes the code clear and easy to parse.)
- For interpolation, we could have one or both of these:

Code: Select all

var := Deref("Hello %username%!")
var := Deref("two plus two equals %2+2%")
var := Deref(" 20%A_Pct% of AutoHotkey users ")
var := Deref('notepad.exe "C:\MyDir\Users`'s File"')
var := Deref("notepad.exe `"C:\MyDir\Users's File`"")

var `= Hello %username%!
var `= two plus two equals %2+2%
var `= `s20%A_Pct% of AutoHotkey users`s
var `= %A_Space%20%A_Pct% of AutoHotkey users%A_Space%
var `= notepad.exe "C:\MyDir\Users's File"
- The cleanness and brevity of continuation sections and auto-concat are fundamental to why I view AHK as a significantly better programming language to program in, versus other languages.

- Re. #Include:
- If #Include used expression syntax, future AHK v1 versions could interpret paths containing " as expressions.
- Note: Looking through the AHK v2 directives, only #If currently supports expression syntax, thus, it is not certain that #Include could or should change to use it. (At present I'm neutral on whether it should remain as is or change.)

- Re. NumPut/structs:
- For a RECT, I'd do something like this:
NumPut([1, 2, 3, 4], RECT, "Int")
- So, I'd only change NumPut (from AHK v1) to allow arrays for the Number/Type parameters.
- I probably wouldn't change NumPut beyond that. For anything else, I'd want ...
- A struct function, could accept a C++-style struct definition string (throwing for unknown types).
- But it would also be useful to be able to define a struct explicitly, with padding specified. With handy syntax to include/omit padding based on the script's bitness, and to handle bit fields.
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: First thoughts after coming back from a long hiatus

19 Oct 2019, 07:36

@fincs
Welcome back!

Continuation sections: I agree, but they also provide some unique capabilities (such as specifying a custom sub-expression to be substituted for each line ending). Like you (I think), I'm a little biased against continuation sections after having written multiple parsers. However, I think that having only limited highlighting within continuation sections is an acceptable cost. For "transpilers", conversion or refactoring tools, I think the problem is similar to preserving comments within a statement, and is not insurmountable. I plan to write tools for assisting conversion from v1, and these tools must understand continuation sections regardless of whether v2 has them.

String interpolation: I've replied in Replacing percent signs.

Directives & #Include: See Directives and expression syntax. The main reason I have not changed it is that I imagine changes to the expression parser/evaluator may supersede any "constant expression" parsing code that I write specifically for directives.

I think that a module system should supplement #Include, not replace it. The syntax of #Include and "module" functionality should be considered separately.

I don't see any issue with libraries providing classes (any more than libraries providing functions), and actually considered removing the function auto-include mechanism. If I had taken a different direction (e.g. combining function and variable namespaces rather than splitting methods and properties), I might have replaced new ClassName() with ClassName(), which could auto-include a class library. Although perhaps having class X implicitly define a function with the effect of X(a*) => X.new(a*) would be another option (and has probably been proposed before).

__Set: You seem to be implying that the property's [parameters] are included directly in the meta-function's parameter list; that is not the case. The first two explicit parameters of __Get, __Set and __Call are always Name, Params, where Params is a single parameter containing an Array. Value is a separate parameter which has a fixed position, unlike before.
Meta-Functions wrote:Params
An Array of parameters. This includes only the parameters between () or [], so may be empty.
v2-changes wrote:Method and property parameters are passed as an Array. This optimizes for chained base/superclass calls and (in combination with MaxParams validation) encourages authors to handle the args. For __set, the value being assigned is passed separately.
I prefer to exclude the implied this parameter, since it must be defined as a method (with DefineMethod or in a class), not merely as a function reference stored in a key-value pair. I intend to revise the terminology at some point.

fincs wrote:(however the object for the class itself is in fact just an instance of Class so its base ought to be Class.Prototype, right?)
Originally it was that way, but I found it was more flexible (and perhaps intuitive) for the class to derive from its base class, so for instance class MyClass is based on the global Object, which is based on Class.Prototype. It allows class objects to participate more in polymorphism, such as for classes derived from Array to inherit the factory method Array.New(), which constructs an object of different native type to Object.New(). It is accurate to say that "you can access static members (fields/methods) of a class from any of its subclasses." Also, Array.HasBase(Object) is true, and the class hierarchy can be discovered at runtime through the base property.
There's some potentially confusing wording in the Class page: "Object is based on Class.Prototype ...
Perhaps it should say "the global class object Object", for clarity.
However (and this is the confusing part) there is still an Object(...) construction function which takes key/value pairs (which then become "own properties"), and the { ... } syntax still constructs these prototype objects.
Object() will probably be removed; at the moment it is essentially an implementation detail of { ... }. It does not take key-value pairs, but property names/values. Regarding {} vs. Map, see Map shorthand.
Plain objects that are neither Map nor Array still seem to accept obj.property (:= value) syntax for modifying or retrieving arbitrary fields, just like an associative array.
All objects (excluding most COM objects) support this, including Map and Array. That aside, what use would a plain object be without its capability to have properties? It could be restricted to properties defined within a class or with DefineProp (which may be extended to permit creating value properties), but under the hood there will always be some kind of associative array. Map does not support obj.key := value by default (by which I mean this would create a property, not a map element), as that would at least partially defeat the point of separating Map from Object (or key-value pairs from properties). obj.x is always a property while obj[x] is always an item/element. A Map can have custom properties relating to the collection without conflicting with keys contained within the collection.
Dynamic properties (i.e. get/set functions in disguise) are treated specially though (i.e. defined with DefineProp).
How could they not be? The main change from v1 is that the struct used to implement these properties is never exposed to the script. In v1, objects gave special treatment to key-value pairs containing Property objects, but not consistently (e.g. an enumerator could be used to retrieve this object, which was never really intended to be used directly). In v2, the script must be explicit about the kind of property, by using DefineProp rather than an assignment. (Perhaps in future the distinction could be made by whether the object passed to DefineProp has a value property vs. get/set, but I chose not to implement that yet since a simple assignment is currently sufficient for value properties.)
I suspect this was probably kept in order to have a convenient mechanism to support class fields (both static and per-instance), however it feels kind of confusing/redundant and/or defeating the point of separating Array/Map into different types.
You've lost me on both points. The separation of static and per-instance properties is achieved by putting per-instance properties into Cls.Prototype. The point of separating Map from Object was to avoid conflicts, such as between arbitrary keys (e.g. words pulled from a file, words in a search index, etc.) and property names like "base" and "length". (If methods and properties hadn't been separated, this would also include method names. For example, a dictionary used when parsing words from a script file could not be passed to a for-loop because "_NewEnum" was one of the words in the file.)

Separating Array and Map from each other and from Object clarifies the roles of objects the script constructs, clarifies the roles of methods provided by each class, and in some cases permits optimizations more easily and to better effect.

ClipboardAll vs Buffer: Clipboard := obj only permits objects explicitly of ClipboardAll type, by design. In theory, it adds a little safety/clarity and might assist with debugging. On the other hand, it is a little inconvenient when reading clipboard data from a file, and (in theory) increases code size by some amount I haven't measured.

COM objects: I am aware the documentation is a bit lacking with regard to information about COM objects in general. I intend to improve that at some point.

Raw interface pointers: I have mixed feelings about this; the primary reason I haven't changed it is that I picture ComObjCreate (when used with an IID) and ComObjQuery as being low-level functions that may be used to create more usable wrappers around specific COM interfaces, where one would be creating a custom wrapper class and not using ComObject(13, punk).

I recently wrote some code separating the ComObject wrapper into an IDispatch wrapper and "ComValue" (everything else), with the latter being extended from Object.Prototype and therefore allowing the script to monkey-patch it, but I wasn't convinced that was the right approach. For instance, the script would need to explicitly modify the wrapper after it is created, unless more mechanisms are added that would enable the script to specify a wrapper class, in which case extending ComValue doesn't gain you much.

Perhaps one answer is to accept a class or factory object in place of an IID string, but I think it doesn't seem much better than putting the ComObjCreate or ComObjQuery call inside a static method. Some of the boilerplate code can be put into a common base class, since (as I noted above) classes can inherit static methods.

For DllCall and ComCall, I thought the cleanest and simplest solution was...
v2-changes wrote:For Ptr*, the parameter's new value is assigned back to the object's Ptr property. This allows constructs such as DllCall(..., "Ptr*", unk := IUnknown.new()), which reduces repetition compared to DllCall(..., "Ptr*", punk), unk := IUnknown.new(punk), and can be used to ensure any output from the function is properly freed (even if an exception is thrown due to the HRESULT return type, although typically the function would not output a non-null pointer in that case).
ComObjError: I intend to remove it when I revise the error handling model (again). Object.ahk/Errors shows some of my ideas.

NumPut and structs: See these posts in particular:
https://www.autohotkey.com/boards/viewtopic.php?p=276463#p276463
https://www.autohotkey.com/boards/viewtopic.php?p=276743#p276743
https://www.autohotkey.com/boards/viewtopic.php?p=297200#p297200 (posted a moment ago)

GuiCreate/Gui.AddCtrl parameter order and Add vs. AddCtrl: I think I've played devil's advocate for this before, but I my current stance on both points is one of indifference. I'm likely to accept a change, especially if it can be shown that there is consensus on each point.

Tab controls: This has been in my private notes since I implemented Tab3. Unfortunately, I think there are some cases where Tab2 works better than Tab3 (with Tab3 being unsuitable in some cases). Combining Tab and Tab2 doesn't save much in terms of documentation or code size.

Gui delimiters: I am indifferent.

IsSet and omitted parameters: A parameter declaration with default value looks like an assignment, and has the same effect when the parameter is omitted: the parameter variable is assigned that value. It is set.

IsSet is only possible due to the implementation of #Warn UseUnset, and its primary purpose is to provide a way to check whether the variable has been initialized without triggering a warning. (In some cases VarSetCapacity can be used instead, but it is insufficient for multiple reasons.) If an omitted parameter is "unset", accessing it may trigger a warning. That should not happen for an explicit default value of "" any more than for an explicit default value of "not empty". Omitting the parameter should be the same as passing its default value. (Of course, every default value is explicit in the current version, and there is no way to declare a single optional parameter without defining a default value.)

I have some experimental code that allows global and (I think?) static variables to be used as parameter defaults. A variable can contain a unique object, whose only purpose is to indicate that a parameter has been omitted - for instance, class missing {}. In general though, I think the lack of ability to distinguish between passing the default value and not passing a value for a formally-defined (not variadic) parameter is not a significant limitation.
User avatar
fincs
Posts: 527
Joined: 30 Sep 2013, 14:17
Location: Seville, Spain
Contact:

Re: First thoughts after coming back from a long hiatus

19 Oct 2019, 17:44

Thanks for the warm welcome everyone :)
lexikos wrote:specifying a custom sub-expression to be substituted for each line ending
I'm not sure how common this situation would be; and honestly if I saw someone doing that I'd probably think they're a bit insane :p
lexikos wrote:However, I think that having only limited highlighting within continuation sections is an acceptable cost.
The fact that the syntax highlighting would not be 100% accurate is something that I would particularly not be happy with. Lack of accurate syntax highlighting is something that has been a big problem in AutoHotkey for ages, and I believe v2 has the opportunity to rectify this.
lexikos wrote:The main reason I have not changed it is that I imagine changes to the expression parser/evaluator may supersede any "constant expression" parsing code that I write specifically for directives.
Ah, so basically it's not changed because it depends on future changes and design decisions. Understood.
lexikos wrote:I think that a module system should supplement #Include, not replace it. The syntax of #Include and "module" functionality should be considered separately.
#Include has its uses and I think it should continue to exist. However I do think that the most common use case of them (importing external libraries) should be given some sort of first class support in line with other programming languages.
lexikos wrote:considered removing the function auto-include mechanism.
I actually saw that post earlier. I thought about it, and I've realised that auto-includes can violate the principle of least surprise and introduce unintended side-effects. So, I would be willing to support a potential removal of this feature; in favour of explicit <> inclusion of standard libraries.
lexikos wrote:Although perhaps having class X implicitly define a function with the effect of X(a*) => X.new(a*) would be another option (and has probably been proposed before).
That is an interesting proposal. It might have implications for some of the standard classes such as Object, Map or Array though (but I think this is a win-win since the constructors should behave in the same way).
lexikos wrote:Params is a single parameter containing an Array. Value is a separate parameter which has a fixed position, unlike before.
I had assumed Params was a stand-in for variadic arguments, but that is not the case. Since it's just a plain Array, then there is no issue at all with the meta-function arguments.
lexikos wrote:but I found it was more flexible (and perhaps intuitive) for the class to derive from its base class, so for instance class MyClass is based on the global Object, which is based on Class.Prototype.
Aha, that makes sense. However, I'm still trying to figure out where Class stands in said class hierarchy. MyClass is based on Object; which is then based on... Class.Prototype; but Class is based on what exactly? Also, I see a potential confusion: objofmyclass.HasBase(MyClass) is false (and what you're supposed to do instead is objofmyclass.HasBase(MyClass.Prototype)
lexikos wrote:Perhaps it should say "the global class object Object", for clarity.
Yeah, that would help. Actually, it would help to make the fact that class objects are separate things a bit more explicitly; and they have their own identity and hierarchy different from that of instance objects.
lexikos wrote:Object() will probably be removed; at the moment it is essentially an implementation detail of { ... }. It does not take key-value pairs, but property names/values. Regarding {} vs. Map, see Map shorthand.
All objects (excluding most COM objects) support this, including Map and Array. That aside, what use would a plain object be without its capability to have properties?
Thank you for pointing me to the Map shorthand discussion, I missed it. My original post confused key-value pairs of a Map obj["key"] with properties obj.prop which are indeed different things and I didn't make this sufficiently clear, sorry. However, I still think that { ... } not yielding anything other than a Map is misleading, since this is JSON-like syntax (and amusingly, JSON mandates quoted strings for keys which reinforces this view). If any of the Map shorthand proposals goes through, we would have three literal object syntaxes: one for arrays, one for maps and one for object. While I don't necessarily disagree here with the current design and I do think it makes sense to have a flexible type that provides the backbone for properties (and methods); I suspect maps vs objects might be a superficial source of confusion for inexperienced users (the fact that they have different syntax definitely helps though). We might see some users compelled to abuse objects as if they were a different way to achieve the same end result provided by maps.
lexikos wrote:How could they not be? The main change from v1 is that the struct used to implement these properties is never exposed to the script. In v1, objects gave special treatment to key-value pairs containing Property objects, but not consistently (e.g. an enumerator could be used to retrieve this object, which was never really intended to be used directly). In v2, the script must be explicit about the kind of property, by using DefineProp rather than an assignment. (Perhaps in future the distinction could be made by whether the object passed to DefineProp has a value property vs. get/set, but I chose not to implement that yet since a simple assignment is currently sufficient for value properties.)
I didn't actually disagree with this and in fact the v2 system is clearer than v1.
lexikos wrote:In theory, it adds a little safety/clarity and might assist with debugging. On the other hand, it is a little inconvenient when reading clipboard data from a file, and (in theory) increases code size by some amount I haven't measured.
I'm really not sure if this little bit of safety or clarity is worth it. From the point of view of consistency, it's strange to have only a certain functionality accept a special tailored type, while everything else accepts a regular buffer.
lexikos wrote:I recently wrote some code separating the ComObject wrapper into an IDispatch wrapper and "ComValue" (everything else), with the latter being extended from Object.Prototype and therefore allowing the script to monkey-patch it, but I wasn't convinced that was the right approach. For instance, the script would need to explicitly modify the wrapper after it is created, unless more mechanisms are added that would enable the script to specify a wrapper class, in which case extending ComValue doesn't gain you much.
I wonder if at this point it's a better idea to ditch using wrappers altogether for COM objects, and bring on IDispatch as a token type. AHK objects are IDispatch already, and some form of querying or metadata could be used to upgrade object accesses to native AHK Invoke. (A small wrapper object will still be needed for other COM types, and your ComValue proposal would fit the bill).
lexikos wrote:I have some experimental code that allows global and (I think?) static variables to be used as parameter defaults. A variable can contain a unique object, whose only purpose is to indicate that a parameter has been omitted - for instance, class missing {}. In general though, I think the lack of ability to distinguish between passing the default value and not passing a value for a formally-defined (not variadic) parameter is not a significant limitation.
The fact that there are built-in functions with non-constant default values for parameters to me is an indication otherwise. It would be ideal if we kept special calling convention behaviors in built-in functions that cannot be emulated in script to a minimum.
fincs
Windows 11 Pro (Version 22H2) | AMD Ryzen 7 3700X with 32 GB of RAM | AutoHotkey v2.0.0 + v1.1.36.02
Get SciTE4AutoHotkey v3.1.0 - [My project list]
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: First thoughts after coming back from a long hiatus

19 Oct 2019, 22:03

fincs wrote:
19 Oct 2019, 17:44
honestly if I saw someone doing that I'd probably think they're a bit insane :p
I do it, so... touché. :HeHe:

Join"`sdelim`s" is preferable to repeating " delim " at the end of every line, for example, but it's generally trivial to replace the default delimiter with StrReplace instead.

Continuation section highlighting: Highlight it accurately, or don't highlight it. I see no problem with highlighting continuation sections using a dedicated colour that cannot be confused with anything else.

Implications of automatic constructor functions: Object, Map and Array already have functions that construct instances of those classes, but the classes do not currently have constructors. I didn't think it was appropriate for every user-defined (and eventually, built-in) class to inherit a default constructor that accepts a list of property name-value pairs. For Map and Array, I just figured it was unnecessary.

Of course, class library authors could add an explicit "constructor" function by convention.

Class is based on what exactly?: Classes are instances of Class, but are derived from their superclass. Since classes are objects, Class must have Object as a base class; it happens to be the direct base.

objofmyclass.HasBase(MyClass): It should be clear from the examples in the documentation. The preferred way to perform an "instance of" check is still with objofmyclass is MyClass. HasBase was added to better support non-class prototype-based inheritance.

Clipboard: Functions and built-in variables generally require specific type(s) of value, or sets of values. Some parameters are number only, and will throw if given the wrong type. Some parameters (probably just the built-in object methods/functions) require a specific type of object and will throw if given the wrong type. Passing the wrong kind of value to the function doesn't make sense, and having the function accept the wrong kind of value doesn't make sense when it can be restricted to the right kind. Like many functions, Clipboard requires a specific type of data (binary clipboard data in a very specific format), or something convertible to string. It simply has different requirements to those other functions, and that is reflected by the type check.

As for everything else accepting a Buffer, what more specialized type could any of those functions accept? They accept or return arbitrary data. A Buffer is a space to put arbitrary data. They accept a memory address or an object encapsulating a memory address (and size). DllCall and SendMessage don't even know what they're doing with the data, or even that the Ptr is really a memory address (it doesn't have to be).

Clipboard also accepts and returns text, which is not equivalent to the data contained within a ClipboardAll object. Perhaps Clipboard shouldn't accept an object; perhaps the ClipboardAll object itself should write its data to the clipboard.

I think your argument about consistency is not sound, so I wonder what exactly the small benefit of the ClipboardAll type "isn't worth". However, I would at least agree that it might not have much value.

Ditching wrappers (don't litter!): I've had thoughts on this, but I consider it low priority. I think the only user-visible aspect for IDispatch is that a new wrapper object is created whenever an IDispatch pointer is returned/passed to the script, therefore two wrappers for the same object might not compare equal. (It's also possible for an object to have two different IDispatch-derived interfaces. I think querying for IUnknown is supposed to be a way to get the object's "identity".)

At the moment, calls resulting from ComObjConnect automatically pass the original wrapper object at the end of the parameter list, partly because of the "identity" problem and partly because some events don't pass the source of the event as a parameter. However, this conflicts with the new restriction on passing too many parameters (which also causes other awkwardness). In retrospect, the extra parameter was never really necessary - it just helps to avoid global variables when the script is using functions rather than an object (which requires v1.1.01+).

Actually, the lifetime of the event sink created by ComObjConnect is currently tied to the wrapper object, and each wrapper can have only one event sink. Events are disconnected by passing the same wrapper object back to ComObjConnect. Before eliminating wrapper objects, this would need to change.

I'm not sure how often scripts need to specify the COM type of a parameter, but I have considered making the mechanism more abstract; i.e. any object could decide how it is "marshaled" to a COM method. If this is done through simple properties, a "ComValue" wrapper might be redundant.

Built-in functions with non-constant default values: Variadic functions can easily replicate this (hence "formally-defined (not variadic) parameter"). Built-in functions are essentially variadic; they may still impose a maximum parameter count, but so can a user-defined function (explicitly, and only at runtime). Lacking that restriction is hardly a limitation.

I am not in favour of make built-in functions less intuitive just to match the current limitations of non-variadic functions. I won't argue against removing such limitations given a suitable way to do so, only that they are insignificant and therefore low priority.

Map: I replied in Map shorthand.

Return to “AutoHotkey Development”

Who is online

Users browsing this forum: No registered users and 33 guests