Changes to %fn%(), fn.call() or func('fn') syntax?

Discuss the future of the AutoHotkey language
sirksel
Posts: 43
Joined: 12 Nov 2013, 23:48

Changes to %fn%(), fn.call() or func('fn') syntax?

20 May 2019, 17:02

Many thanks to @lexikos for all the work he has done to enable fat arrow notation (e.g., fn(x,y) => x*y and x => x*2) and method/property shorthand (e.g., property[] => expr()). It takes Func.Bind() to a whole new level, and really enables much more efficient functional programming (e.g., map, filter, compose, pipe, transduce, etc.). I don't think fat arrow in AHK allows blocks, but multi-statement makes most functional constructs possible anyhow. That said, a couple small sticking points remain that make functional programming in AHK a little less streamlined/clear than it could be:

1. Function objects can only be called as fn.call() or %fn%() rather than fn(). Unlike a lot of other languages that have first-class functions, AHK seems to require that every function composition, partial, etc. (anything returned as a function/boundfunc/closure object) must be called in this more roundabout way.

2. Explicitly defined and built-in functions can't be referenced/passed without a Func() wrapper creating the function object. It makes for slightly awkward pipe/compose constructs when mixing defined functions with other intermediate (and possibly non-global) function objects, especially when trying to compose in a pointfree style. For example: strbaz := compose(func('strsplit'), strbar, func('strupper'), strfoo). I've also tried doing this: strbaz := compose('strsplit', strbar, 'strupper', strfoo), then checking param type and auto-wrapping. It reads slightly better, especially when the composition is nested or more complex, but it's a bad option because it's more error-prone and bypasses the interpreter optimizations.

Any thoughts? Are these design decisions that you plan to preserve in v2? Are they possibly part of your consideration for changes, along the lines of the other %% discussions in the Objects preview thread?
lexikos
Posts: 6488
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: Changes to %fn%(), fn.call() or func('fn') syntax?

22 May 2019, 04:31

I'm sure this has come up multiple times, but I wasn't able to find a good topic to reference.

1. %% or .Call is required when calling a value (typically contained by a variable), regardless of the type. What you want is for xxx() to permit both function names and variable names without any differentiation. This requires combining the two into one namespace, which implies that you can no longer have a function and a variable with the same name in the same scope. I believe these are the main drawbacks:
  • Assignments accidentally overwriting functions. This can be mitigated by making function identifiers read-only (I mention it because it's a common issue in some languages, and would be even more so here due to case insensitivity).
  • Variables accidentally shadowing functions by the same name. If the variable does not contain a function, this would cause a runtime error when the function call is attempted. If the variable does contain a function but not the one the author intended to call, the error is harder to detect.
  • The reduction in load-time error checking. For example, if an author makes a typo in the function name, it might be difficult to catch the error at load time since the author might have been intending to call a variable. (Aside from the above problems, load-time validation of function parameters can still take place, provided that it is not possible to overwrite a function at runtime.)
2. Generally Func returns the function itself; the same object that a static call such as MsgBox() references. Even if you were able to reference the function directly by name, what you would get is exactly the same. There is no wrapping or creating, except in the case where the function contains upvars, where Func creates a Closure referencing the original function and captured variables. (The current implementation permits a nested function to be called directly without the overhead of creating a closure.)


I had written the following in v2-thoughts, but hadn't updated it online (for no particular reason):


Detecting mispelled function names at load time is a useful feature of the language, but currently only applies to direct calls. References such as Func("name") or "name", if mispelled, will only be detected when the script attempts to call them (which might not be where they are referenced).

Func("name") is optimized (in v2) to resolve at load time, but cannot raise a load time error when the function does not exist because it is not an error. For instance, name can be defined in an optional #include *i, and Func is how it would be resolved to be called. If it raised a load time error, this would not work:

Code: Select all

    if f := Func("name")  ; ← load time error
        f.Call()
Nor would this:

Code: Select all

    if IsFunc("name")
        MyRegisterEvent(Func("name"))  ; ← load time error
The most obvious solution is to add new syntax for a function reference which would be caught as an error at load time if the function does not exist.
  • fincs suggested @name, as in OnExit(@ExitHandler) or greet := @MsgBox.Bind("Hello").
  • Another idea is Func'name', as in OnExit(Func'ExitHandler') or greet := Func"MsgBox".Bind("Hello"). Although not currently valid, it might be difficult to visually distinguish from auto-concat.
iseahound
Posts: 446
Joined: 13 Aug 2016, 21:04
GitHub: iseahound

Re: Changes to %fn%(), fn.call() or func('fn') syntax?

05 Aug 2019, 18:18

I don't know if this is naïve but would it be possible to "lift" the variable into the function namespace? There already exists some oddly specific behavior: namely a function and a class can have the same name, but if the function and the class are nested inside a bigger class, this fails.

Scenario #1

Code: Select all

s()

; You can comment this out
s() {
   MsgBox % A_ThisFunc
}

; But class s can never be called...
class s extends functor {
   call(terms*) {
      MsgBox % A_ThisFunc
   }
}

class functor {
   __Call(self, terms*) {
      if (self == "")
         return this.call(terms*)
      if IsObject(self)
         return this.call(self, terms*)
   }
}
Scenario #2

Code: Select all

namespace.s()

class namespace {

   ; static s := A_ThisFunc

   ; Either comment out function s()
   s() {
   	MsgBox % A_ThisFunc
   }

   ; Or comment out class s
   class s extends functor {
      call(terms*) {
         MsgBox % A_ThisFunc
      }
   }

}

class functor {
   __Call(self, terms*) {
      if (self == "")
         return this.call(terms*)
      if IsObject(self)
         return this.call(self, terms*)
   }
}
In Scenario #1 variable space and class space are merged together (classes can be overwritten with simple assignment!) and function space is separate. However in Scenario #2, they start to mix together. Specifically when operations are conducted inside a nested class, only one of the three is permitted: the variable s, the function s() or the class s.

One solution to this problem would be to keep the existing variable and function spaces separate, and letting the class space be the "push-out" of the two spaces. In other words if we imagine the spaces as lists with a check mark next to them, the variable s has a check mark in the column "isVariable?", the function s() has a check mark in the column "isFunction?", and the class s has a check mark in both the variable and function columns. Checking for errors would allow a function and variable declaration of the same name, but disallow a class and a variable of the same name, and disallow a class and a function of the same name. Instead, classes could be called with className() exactly like a function - something that currently is not possible right now. Finally it would standardize behavior in Scenario 1 & 2, things inside nested classes aren't treated differently from those outside.

As a side note: Partial functions would be both variables and functions - a boundFunc could be called boundFunc() and referenced as boundFunc. Classes would have a additional "super" flag that boundfuncs lack, allowing them to be visible anywhere. (super lexical scoping?)

Sorry if this doesn't make any sense.
User avatar
nnnik
Posts: 4242
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Changes to %fn%(), fn.call() or func('fn') syntax?

06 Aug 2019, 01:18

A function inside a class doesnt exist. A function when defined inside a class is a method. Variable space does not exist within classes - they are object members and object space.

None of that is a good decision. Why does object space differ so strongly from the script space? Why is there no common mechanism to just address the outer space between the 2? Why do object keys differ so strongly from normal variables?

I agree with you that functions need to get pulled down from their high horse and get inserted into normal variable space.
This also wouldn't change current behavior if we made specific entries into the variable space read only.
For example function and class entries.

A while back I argued against making classes read only but I changed my mind. Overwriting a class is not that important.
Classes could also take on the role of a namespace - instead of creating a new mechanic with new operators and new implications etc...
Class space should be entirely consistent with script space on most things (e. g. It doesnt need to support labels, hotkeys, preprocessor instructions, A_variables...)
Recommends AHK Studio
lexikos
Posts: 6488
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: Changes to %fn%(), fn.call() or func('fn') syntax?

10 Aug 2019, 00:07

iseahound wrote:
05 Aug 2019, 18:18
... but if the function and the class are nested inside a bigger class, this fails.
Not in the next alpha. Like with Objects.ahk (but without the restrictions imposed on class definitions by the current alpha), properties and methods are separate. Static members and instance members are also separate. A nested class, instance property, static method and instance method can all coexist in the same class with the same name.
There already exists some oddly specific behavior:
It is not that specific. A function and variable can coexist, but in the current alpha and v1, a method and property cannot coexist.
nnnik wrote:
06 Aug 2019, 01:18
This also wouldn't change current behavior if we made specific entries into the variable space read only.
For example function and class entries.
If a script never creates a variable (by assignment or merely by reference) with the same name as a function, that script's behaviour would not change. If a variable and a function existed with the same name, the script would no longer work; it would either fail to execute, or execute incorrectly. So "wouldn't change current behavior" is either incorrect or not particularly meaningful.

Making function-variables read-only solves one problem: accidental overwriting of functions by assignment.


When an undefined function is encountered at load-time, the possible responses are:
  • Attempt to include a file from stdlib to pull in a function definition. On failure, do either of the following.
  • Raise an error.
  • Do nothing (until run-time).
Combining the namespaces does not necessarily imply that there is a variable for every function definition. A name could be bound to either a variable or a function. An identifier for which no function definition exists could be either a variable or an undefined function (which may or may not be a variable).

If they can't be differentiated based on syntax, there are some options.
  1. Assume it is a variable which might be assigned a function object later. Valuable load-time error detection is lost. Auto-include could be attempted first, but that could be problematic.
  2. If auto-include fails, raise an error. Flexibility is reduced - variables still cannot be called directly.
  3. A mixture, depending on whether the variable is declared (could be a normal declaration or a new kind of declaration which shows the intent). I suppose this implies more complexity, and some compromises (less convenience and more clarity than #1, but still less clarity than syntax dedicated to function references).
  4. A mixture, depending on whether an assignment is detected somewhere. I suppose this has a balance of convenience, flexibility and error-checking capability, but increases complexity and lacks clarity.
User avatar
nnnik
Posts: 4242
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Changes to %fn%(), fn.call() or func('fn') syntax?

10 Aug 2019, 04:16

Thanks for the thorough explanation. I missed a few points while making my post.

The current behavior will change. Im not quite sure what I meant with that note. I think it was supposed to say something like "functions still can't get overwritten by assignments".
Im not convinced that keeping the auto-include setting active is good practice - nonetheless it adds value to the language imo. If that value outweighs the costs that it introduces is another question.

We have the goals of: Allowing normal functions to work, doing runtime/loadtime error checking, auto-including and finally allowing dynamic function references to be called like normal functions.
  1. Allowing normal functions to work is obviously priority nr. 1
    Making some variables read only would ensure this to some degree - (it would also help with some other problems)
  2. runtime error checking is my next priority. If something goes wrong it should at the very least be detected at runtime.
    Just saying nothing is not an option imo.
  3. auto-include and calling dynamic functions like normal ones are tied for this spot. I think calling dynamic functions like normal ones is slightly more important.
    But I dont think its enough to place it above auto-including. Both should still work. Since they currently conflict in any of the alternatives you have shown.
  4. load time error checking it's nice - but most of the language doesn't have load time error checking.
    Keeping it here and favoring it over a new possible syntax would be a bad decision imo.
If we imagine a language that ideally covers all of these priorities in order:
Checks at load time to some degree in a function fool absolutely cannot be a dynamic call and cannot be from a dynamic include.
When encountering the call loads the auto-include or calls the dynamic reference stored/throws an error if an assignment has been made to the variable but it didnt result in a function reference.
Throws an error if no auto-include was possible.

Sadly that conflicts with the current code of AHK v2 (judging from what you said). Since includes need to be included right from the start from the script.
The interpreter would have to evaluate whether that dynamic call is intended to be a dynamic call or call a function in an auto-include before it can fully evaluate that.
You can create code that can make good guesses but in the end %var% is what would break any serious attempt at doing so.
Also I can imagine that that code would be a nightmare to maintain.

Unless something changes with the foundations of the language, this suggestion will have serious side-effects that cannot be mitigated easily.
So in the end "not in v2" is how we could summarize this.
Recommends AHK Studio
iseahound
Posts: 446
Joined: 13 Aug 2016, 21:04
GitHub: iseahound

Re: Changes to %fn%(), fn.call() or func('fn') syntax?

10 Aug 2019, 10:14

I’m still in favor for keeping variable space and function space separate for variables and functions respectively. Function objects should exist across both spaces with one space being equal to referencing the functor and the other being equal to executing or calling the functor. I think other languages resort to lazy evaluation in this case. Yes there probably is not a satisfying low complexity form of run time checking.

Return to “AutoHotkey v2 Development”

Who is online

Users browsing this forum: No registered users and 9 guests