Preview of changes: scope, function and variable references

Discuss the future of the AutoHotkey language
User avatar
vvhitevvizard
Posts: 454
Joined: 25 Nov 2018, 10:15
Location: Russia

Re: Preview of changes: scope, function and variable references

Post by vvhitevvizard » 15 Mar 2021, 07:49

lexikos wrote:
14 Mar 2021, 23:30
I think the documentation is very clear about it. (...)
Global variables which are only read by the function, not assigned or used with the reference operator (&).
Yeah. Those I've read. And I'm afraid I don't follow it yet. Those just state new rules (and exceptions).
I was hoping u would provide me with some additional rationale why is it so.
From the user point of view:
1. we've got new rules for the game. ok. every local/static declaration OR assignment (read as: any "write" operation) resolves a variable as local. simple. understood. ok.
2. & operator is used in the expression ("right side"), its not an assignment ("left side") for that var, its a "read" operation, we just want to obtain an address of that var (pointer to that var).
@kczx3 wrote concerns about isset's VarRef parameter. It gives me mixed feelings as well.
For example, VarSetStrCapacity's VarRef is reasonable. & here implies that the var might be modified by the function call.
Point against IsSet's VarRef: the function is NOT modifying the var's content. Point in favor: We don't use its content either.
Previously IsSet(var) would mark var as "assigned somewhere" to suppress the warning.
It's actually interesting to know in order to understand internal motives.

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Preview of changes: scope, function and variable references

Post by kczx3 » 15 Mar 2021, 07:59

vvhitevvizard wrote:@kczx3 wrote concerns about isset's VarRef parameter. It gives me mixed feelings as well.
I wouldn't say it was a concern. I just didn't understand why that had to change occurred and it was a lacking in understanding of the VarRef work. To me, it just seemed weird trying to get a ref to a variable that didn't exist. As Lexikos mentioned, the work of masking the variable moved from being done by IsSet to handled by the &. I suppose its not any weirder than calling a function and passing an undefined variable. But due to v1, that's something we are all accustomed to being able to do and get an empty string. That said, it makes sense to me to have the IsSet function do that work - as its name implies.

User avatar
vvhitevvizard
Posts: 454
Joined: 25 Nov 2018, 10:15
Location: Russia

Re: Preview of changes: scope, function and variable references

Post by vvhitevvizard » 15 Mar 2021, 08:11

kczx3 wrote:
15 Mar 2021, 07:59
As Lexikos mentioned, the work of masking the variable moved from being done by IsSet to handled by the &.
It's acceptable. The unified & rule.
BTW, what do u think of my point of view concerning & operator changing a global variable's status within the current scope?

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Preview of changes: scope, function and variable references

Post by kczx3 » 15 Mar 2021, 08:14

@vvhitevvizard
I guess I'm not sure I follow exactly. Can you share a very short example that exemplifies the issue so I can provide an opinion?

User avatar
vvhitevvizard
Posts: 454
Joined: 25 Nov 2018, 10:15
Location: Russia

Re: Preview of changes: scope, function and variable references

Post by vvhitevvizard » 15 Mar 2021, 08:16

Full example was in this post under section 2. Also in this post at the very bottom, this post at the top.
, this post at the top.

Code: Select all

msgbox(isset(&logger)) ;uncomment this to make log local
msgbox(type(logger)) ;class (ok) OR (if u uncomment the line above) "this var has not been assigned a value. specifically: local log (same as a global)"
I fail to see why isset(&logger) statement resolves logger as local for the subsequent operations? the 2nd line behaves as if isset declared a local uninitialized variable.
So uncommenting isset(&logger) line makes the following msgbox(type(logger)) to address a local shadow of the same name logger.

Simply put, after & "read" operation of a global var the following "read" operations to that var considered towards a local var. IMHO, its counter-intuitive. Do I miss some logic?

User avatar
vvhitevvizard
Posts: 454
Joined: 25 Nov 2018, 10:15
Location: Russia

Re: Preview of changes: scope, function and variable references

Post by vvhitevvizard » 16 Mar 2021, 01:23

Variables and functions are combined.
Properties and methods are combined
(...)
Use MyFunc in place of Func("MyFunc").
Use MyFunc in place of "MyFunc" when passing the function to any built-in function such as SetTimer or Hotkey. Passing a name (string) is no longer supported.
Use myVar() in place of %myVar%()
@lexikos In the future updates, do u plan to address Object.GetMethod("literal name") and other object methods syntax to additionally accept non-literal names by expression obj.method rendering it similar to function references syntax?

lexikos
Posts: 9554
Joined: 30 Sep 2013, 04:07
Contact:

Re: Preview of changes: scope, function and variable references

Post by lexikos » 16 Mar 2021, 06:03

vvhitevvizard wrote:
15 Mar 2021, 07:49
Yeah. Those I've read. [...]
I was hoping u would provide me with some additional rationale why is it so.
So why did you say
"the 2nd line behaves as if isset declared a local uninitialized variable." and not
"the 2nd line behaves as if &logger declared a local uninitialized variable.", hmm?

If you want the right answer, ask the right question, don't just hope for it.


The rationale is complex and involves most of the changes presented in this topic, as they are interrelated. If &var could default to taking a non-super global variable's reference without declaration, a number of things would not work as well as they do - you would not have any of the benefits of the new changes, as I would have left it as merely an experiment.

The use of &var often makes it impossible for the parser to determine whether the variable is going to be assigned a value. Even if the parser could detect it in some cases, simple rules are better; easier to implement and maintain, easier to explain, easier to understand.

The primary use of &var is to assign to the variable indirectly, usually by passing it to a function. Having them implicitly assign to (non-super) global variables would introduce problems not present in previous versions.

IsSet is either one of very few exceptions, or the only exception (I can't think of any others at the moment). You "know" that IsSet(&var) doesn't assign to var, but maybe it does. As I said before, "IsSet is a function, and can be shadowed by a local variable or user defined function." Even if we assume that IsSet never assigns to the var, making this an exception for the "&var makes var local by default" rule would be harmful. It would add complication to the rules, which makes them harder to understand, and more bothersome to document and learn.

I know that it is not ideal to require a declaration just to check if a global variable is set, when reading its already-set value does not require declaration. As I said, I will think about alternative solutions.

2. & operator is used in the expression ("right side"), its not an assignment ("left side") for that var, its a "read" operation, we just want to obtain an address of that var (pointer to that var).
If &var was an assignment, the documentation could say
... only read by the function, not assigned or used with the reference operator (&).
... However, if a variable is used in an assignment or with the reference operator (&), it is automatically local by default.
It says what it says because &var is neither a read operation nor a write operation. You are not using or changing the variable's value.

You do not read a variable to obtain its address. In order to read a value, the program must know where it is.

kczx3 wrote:To me, it just seemed weird trying to get a ref to a variable that didn't exist.
That would be weird, but that isn't happening. How do you check if a variable which doesn't exist is set? Asking the question "is this variable set?" implies that the variable exists (especially when you're doing it with a plain variable reference, not a name within a string). And in fact, it does. "In AutoHotkey, variables are created simply by using them." The v1 documentation says "Any non-dynamic reference to a variable creates that variable the moment the script launches." This is actually still relevant.

The variable exists, but maybe the value doesn't.

vvhitevvizard wrote:In the future updates, do u plan to address Object.GetMethod("literal name") and other object methods syntax to additionally accept non-literal names by expression obj.method rendering it similar to function references syntax?
I had to read this several times to make any sense of it. "Non-literal names" are presumably names substituted with %...%. There is no "function reference syntax"; (MsgBox) gives you a reference to a function, but this is just variable reference syntax.

I think what you are asking about (even if you didn't know it) is the use of %...% to evaluate an expression, instead of just a variable name. No, I have no plan to address this. There might eventually be a capability like eval() from other languages, but I doubt I would ever make %...% shorthand for eval.

obj.method is not a "non-literal name". It is (in the right context) an expression, consisting of a variable reference and property name, with the type of operation depending on the context. For instance, (obj.method) just retrieves a property, while (obj.method()) calls it and (obj.method := x) assigns it. If a sub-expression was placed inside a string and evaluated with %...%, it would have to be done one of two ways:
  • The sub-expression is parsed and evaluated independently, and the result is then used within the larger expression. Effectively, x := "obj.method", %x%() would be equivalent to (obj.method)(), which will fail if obj.method is actually a method, since this is omitted.
  • The sub-expression is parsed within the context of the larger expression, which would require delaying parsing of at least part of the outer expression. For instance, %x%(1) could be obj.method(1), funcname(1), 1 + (1), -(1), etc. This would be complicated and not worthwhile.

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Preview of changes: scope, function and variable references

Post by sirksel » 24 Mar 2021, 17:28

@lexikos, as I'm restructuring my libraries for these latest changes, I'm trying to decide where to put declarations of certain derivative global functions. I think I understand the current state of the syntax evolution fairly well now, but am trying to figure out the best practice to use. To revive a prior simple example, consider that I have a set of global functions defined in the main scope (alphabetical order works here, but not always for derivative functions):

Code: Select all

;===== GLOBAL FUNCTIONS ========================
isDiv(y,x) => mod(x,y) == 0   ;is divisible by
isEven := isDiv.bind(2)   ;is even number
isOdd := neg(isEven)   ;is odd number
neg(f) => (x*) => !f(x*)   ;negate (function factory)
These dynamic function definitions now work great (without excessive symbols), thanks to your latest changes. This approach works ok until some static __new() needs one of these functions. In that case, isEven() and isOdd() are unavailable until I create a new "class" with a static __new() of its own right at the top of the script (since these execute in the order that the classes are defined):

Code: Select all

class __libsetup {
  static __new() {
    global
    isEven := isDiv.bind(2)   ;is even number
    isOdd := neg(isEven)   ;is odd number
  }
}
;===== GLOBAL FUNCTIONS ========================
isDiv(y,x) => mod(x,y) == 0   ;is divisible by
neg(f) => (x*) => !f(x*)   ;negate (function factory)
This works, but now the code is split up across two potentially distant sections. So then I thought... "well... maybe I just move all the functions up there," kind of like this:

Code: Select all

class __libsetup {
  static __new() {
    global
    ;non-derivative functions listed first
    isDiv := (y,x) => mod(x,y) == 0   ;is divisible by
    neg := f => (x*) => !f(x*)   ;negate (function factory)
    
    ;derivative functions listed later, in order of dependency
    isEven := isDiv.bind(2)   ;is even number
    isOdd := neg(isEven)   ;is odd number
  }
}
This is a little better, since they're all in the same section. Still slightly suboptimal because (like Python, and unlike Haskell) the derivative functions must still be defined in dependency order. As far as I know there's no way to reserve these definitions at the moment the script launches... which is bad I suppose for this situation, but works out well for other more dynamic use cases.

So this static __new approach works for the simplest functions. However, as soon as one of these functions needs a control flow statement, it doesn't work anymore. I was trying to think if there is an elegant/performant way to define and assign a non-fat-arrow function (in full function notation) to a global var from within a static __new.

I do remember you indicating at some point that you might later be allowing full function notation in more places. Or was it code blocks? I can't find the reference to that discussion right now, despite trying. Anyhow, just wondered if you had any suggestions (given today's v2 state, or the future state of it in your mind) as to how to best handle these placements? Many thanks!

---
EDIT: I had one other thought. Maybe I arrange them in module-like namespaces created with "classes" and static properties, as below? Ignore for a moment that these example functions happen to work nicely as DefineProp() extensions of Func and Integer... I'm attempting a "pure functional" approach for the general case, in which the function parameters may not be this cleanly of the same data type.

Code: Select all

class fun  {
  static neg := (_,f) => (x*) => !f(x*)   ;negate (function factory)
}
class num {
  static isDiv := (_,y,x) => mod(x,y) == 0   ;is divisible by
  static isEven := num.isDiv.bind(,2)   ;is even number
  static isOdd := fun.neg(num.isEven)   ;is odd number
}
Except for the super-ugly discard of this, this approach might be slightly better, but the derivative functions I think still require the dependency-order declaration. I tried doing this as function getters (=> rather than :=)instead, as an alternative, but I couldn't find a good way to get that to work. Any thoughts on the direction of this approach?
Last edited by sirksel on 24 Mar 2021, 20:28, edited 2 times in total.

swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: Preview of changes: scope, function and variable references

Post by swagfag » 24 Mar 2021, 19:31

u can leak them so:

Code: Select all

class __libsetup {
  static __new() {
    global

    global isDiv
    isDiv(y,x) => mod(x,y) == 0   ;is divisible by
    
    isEven := isDiv.bind(2)   ;is even number
    isOdd := neg(isEven)   ;is odd number
    
    global neg
    neg(f) => (x*) => !f(x*)   ;negate (function factory)
  }
}

it would be nice if there was a way to mark callables assigned to variables somehow, so they too could enjoy the same write protection regular function declarations do(perhaps a keyword fn or something, this would also help with syntax highlighting and code inspection for editors)

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Preview of changes: scope, function and variable references

Post by sirksel » 24 Mar 2021, 19:56

Thanks @swagfag. Sorry that my edit and your answer crossed while I was editing. Can you take a look at my edit at the end of the prior post? That's no better, correct?

As for your answer, I hadn't even considered this. What does the more specific global declaration do when the method is already assume-global? I'm not sure I understand what this does or how it does it?

And this whole static __new approach still fails as soon as you need a throw or while or other control flow in one of the functions, I think. Is there any workaround for that case?

swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: Preview of changes: scope, function and variable references

Post by swagfag » 24 Mar 2021, 20:29

u wanted to stuff functions inside a class's static __New() to keep similar functions grouped together.
then u found out that in order to leak them into the global namespace from an assume-global function, the functions have to be stored in plain variables.
then u found out u could only do that for fat-arrow function(store them in variables, that is).
now im telling u to move ur regular declarations, ie:

Code: Select all

f() => abc()
; and
g() {
	return xyz()
}
back into the static initializer and explicitly declare those function names global f, g.
doing that will leak them into the global namespace, solving both ur problems, at the expense of having to write the function name manually an additional time(and remembering not to forget doing so).
Except for the super-ugly discard of this, this approach seems to look slightly little better,
idk, looks gimmicky to me. besides, who puts dots in functional code
I think this option performs much worse
the additional cost is the object member access and ferrying more parameters(this)
since the BoundFuncs and Closures are created every time the properties are called, rather than once. Correct?
no
static __new approach still fails as soon as you need a throw or while or other control flow in one of the functions, I think. Is there any workaround for that case?
yes, just use regular functions as described above

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Preview of changes: scope, function and variable references

Post by sirksel » 24 Mar 2021, 20:47

Thanks @swagfag. That's what I wasn't following. In the example you gave, I can write the functions in either long or short form... such as:

Code: Select all

    global neg
    neg(f) {
      return (x*) => !f(x*)   ;negate (function factory)
    }
So I can put it all in __static new and still use flow control statements in long-form functions wherever necessary. And here I thought I fully understood the nuances of the sytax... :) Many thanks!

lexikos
Posts: 9554
Joined: 30 Sep 2013, 04:07
Contact:

Re: Preview of changes: scope, function and variable references

Post by lexikos » 25 Mar 2021, 22:48

The ability to "leak" a local function into global scope (or make it static) by preceding it with a declaration is a bug. "Bad Things" will happen if the function is a closure. The declaration is intended to be detected as a duplicate at load time, but is only detected if the order is reversed.

User defined "constants" are planned. Making syntax specific to "callable" constants doesn't make a lot of sense. Any constant is callable if its value has a Call method. For syntax highlighting and similar: It is possible through static analysis to identify which variables are directly assigned a function by name or fat arrow syntax, and it should be easier for constants as they would be guaranteed to have only one assignment/initializer.

Ideally classes would be initialized on first use to reduce issues caused by order of initialization. I deferred doing it that way because of complexity. I would not recommend relying heavily on the current initialization timing of static __New.

An alternative is to define each function object as a class. If any boilerplate initialization code is needed, it can be placed in a base class. __New is called for each class even if the method itself is inherited (or you can do the work in static Call).

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Preview of changes: scope, function and variable references

Post by sirksel » 26 Mar 2021, 00:27

@lexikos, thanks for letting us know that. I was about 50 or so functions in to my refactor... leaking them all from static __New. I will revert and go back to the drawing board.

In your constants discussion, is it safe to assume that a constant assigned to a function object or derivative function would be available everywhere regardless of where in the script it was defined? In other words, in my prior example, could I put const isEven := isDiv.Bind(2) (or whatever the keyword will be) anywhere in the code and it would still be available to functions defined above it, to other similarly defined constants, and to the static __New initializers of classes?

Also, I'm still trying to figure out how to finish my refactor, given that I shouldn't rely on the current initialization timing of static __New. I've got a whole family of functions that need to be available everywhere and, given the latest improvements, I've refactored them to be function objects derived from other function objects (like the isEven example above). I'm trying to figure out how/where to define them so they can be available everywhere, including to class static __New methods.

I'm trying to unpack your comments re function object as a class. Are you thinking like this... which you must not be because I can't get this to work...

Code: Select all

class isEven {
  static call := isDiv.bind(2)
}
Many of these are currently one-liners that build on each other, so this (or whatever the right syntax is) probably makes the code a lot longer. Is this the only good option that is likely to survive to the next version of AHK? Sorry if I'm not catching on as quickly as I should. Thanks again for the time and all the great improvements to the language.

lexikos
Posts: 9554
Joined: 30 Sep 2013, 04:07
Contact:

Re: Preview of changes: scope, function and variable references

Post by lexikos » 26 Mar 2021, 07:07

Static __new cannot rely on any previous code having already been evaluated, unless that code is in (or called by) static __new. You can only work with what you have right now, but that does allow you some flexibility if you control the overall script and conventions. I suppose the options are:
  • Don't use static __new at all.
  • Use static __new in a way that guarantees other important initialization code is executed first.
  • Design your dependencies in a way that they do not need to be initialized prior to use.


What are you using static __new for?

Static variable initializers in v1 are unconditionally evaluated before the auto-execute section, so they get used for that purpose; but that's not their purpose. Now they don't work that way, but static __new does, so it gets used for that purpose. But the purpose of static __new is to initialize the class, not to execute code on startup. To do that, you just need to use global code.

Classes could instead be initialized (just calling __init and __new) when their class declaration is reached during execution of global code. I suppose execution of the script would be easier to follow, and closer to other scripting languages. The obvious downside is that classes which require initialization would need to be defined above any global code that depends on them; but the same issue exists for any variables created by assignment, such as your isEven. I suppose these problems can normally be solved by shuffling code or using #includes. Maybe implementing modules in the language could be part of a solution.



For classes, it seems feasible to initialize on first use. Doing this with meta-functions is tricky, and requires not actually defining anything in the class itself... or so it may seem. Object.ahk did it by requiring method and property definitions be placed inside a nested class. However, it can be done a little more transparently with static __new, at least in theory, as follows:
  • Define one class above all other classes, with static __new(). In this method, define the Object.__new method. This will be called once for each class definition which lacks its own static __new.
  • Avoid directly using static __new() in any other class if it depends on other classes. Instead, use static __FirstUse().
  • In the Object.__new implementation function, if this (the class being initialized) has no __FirstUse method, do nothing.
  • Otherwise, replace all predefined properties of this (the class being initialized) with a set of meta-functions which restore the predefined properties, call this.__FirstUse() and then invoke the appropriate property. DefineProp could be used to replace the properties, but that wouldn't detect attempts to access properties which are not yet defined (i.e. properties that are created by __FirstUse).
The end result would be that __FirstUse() would be called when something invokes the class, such as by accessing a static property or calling the class to construct an instance.

Proof of concept (doesn't account for base classes):

Code: Select all

class _FirstUse_ {
	static __new() {
		Object.__new := new_class
		new_class(this) {
			if !HasProp(this, '__FirstUse')
				return
			static getDesc := Object.Prototype.GetOwnPropDesc
			static delete := Object.Prototype.DeleteProp
			static define := Object.Prototype.DefineProp
			backup := Map()
			for name in ObjOwnProps(this) {
				backup[name] := getDesc(this, name)
			}
			for name in backup
				delete(this, name)
			static meta_names := ['call', '__get', '__set', '__call']
			for name in meta_names
				define(this, name, {call: %name%})
			fixup(this) {
				for name in meta_names
					delete(this, name)
				for name, desc in backup
					define(this, name, desc)
				this.__FirstUse()
			}
			call(this, p*) {
				fixup(this)
				return this(p*)
			}
			__get(this, name, p) {
				fixup(this)
				return this.%name%[p*]
			}
			__set(this, name, p, value) {
				fixup(this)
				this.%name%[p*] := value
			}
			__call(this, name, p) {
				fixup(this)
				return this.%name%(p*)
			}
		}
	}
}

Code: Select all

class A {
	static __FirstUse() {
		MsgBox "Init A"
	}
	static Hello() => MsgBox("Hi")
}

class B {
	static __FirstUse() {
		MsgBox "Init B"
	}
	prop := 42
}

MsgBox "Global code"
A.Hello()  ; "Init A" followed by "Hi"
MsgBox B().prop  ; "Init B" followed by "42"
This was interesting, but it probably would have been easier to implement it natively. :facepalm:

sirksel wrote:I can't get this to work...
The call method receives an additional parameter: this (isEven itself or a subclass). You can't simply set a global function as call and have the object behave as that global function. The class itself has to be a proper function object. However, it can be made that way at runtime, rather than by defining a traditional call method. That's what I meant by "boilerplate initialization code".

Code: Select all

MsgBox isEven(1)
MsgBox isEven(2)

class isEven extends sif {
	call := isDiv.bind(2)
}

isDiv(y,x) => mod(x,y) == 0

class sif { ; Self-Initializing Function
	static call(p*) {
		; Get the "instance" variables - just 'call' in this case.
		_ := super.call()
		; Drop 'this' from the parameter list when called.
		this.DefineProp('call', {call: ((this, f, p*) => f(p*)).Bind( , _.call)})
		; Apply the first call (subsequent calls are direct).
		return this(p*)
	}
}
I think you're better off composing functions the original way: define functions that call other functions. That automatically gives you load-time validation of the parameter count, makes it read-only (so does the above), and defines MinParams, MaxParams, etc. (which could be done manually by a class like the one above).

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Preview of changes: scope, function and variable references

Post by kczx3 » 26 Mar 2021, 07:28

lexikos wrote:This was interesting, but it probably would have been easier to implement it natively. :facepalm:
Lol

iseahound
Posts: 1434
Joined: 13 Aug 2016, 21:04
Contact:

Re: Preview of changes: scope, function and variable references

Post by iseahound » 26 Mar 2021, 12:13

I remember asking for something like that and nnnik wrote a wrapper for v1. The main benefit as I understood then, was to get all the declarations out of the way, but ideally should also prevent unused code from running. Either way, AutoHotkey is very fast (and small!), so it seemed to be okay to initialize every object at startup without too much slowdown.

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Preview of changes: scope, function and variable references

Post by sirksel » 27 Mar 2021, 00:28

Thanks everyone for the thought-provoking code snippets. Each one of these gives me not only great starters for handling the various situations, but also a much better understanding of how the object model works... and how it was not intended to work.

I'm beginning to see the light in @lexikos' comment that I'm better off composing functions the traditional way. I was originally hoping I could refactor to a more functional composition style to take fuller advantage of the new (and most excellent!) streamlined function object syntax. It seems so very close...

As I was pondering these examples you gave me, I thought of two alternatives you all might have already considered:

1. A function object assignment operator. In the same way that the property getter syntax unconditionally defines a unique function object, regardless of position of declaration, could the same or similar syntax be allowed in the global scope? In other words, fat arrow with no preceding parentheses, allowing declaration of composed functions in any order:

Code: Select all

;===== GLOBAL FUNCTIONS ========================
isOdd => neg(isEven)   ;is odd number (proposed "function assignment")
isEven => isDiv.bind(2)   ;is even number (proposed "function assignment")
isDiv(y,x) => mod(x,y) == 0   ;is divisible by
neg(f) => (x*) => !f(x*)   ;negate
Maybe this might even solve the issue of availability of composed functions in objects' static __New code, since it might be possible for these function assignments to be evaluated similarly or at the same time as the global functions themselves? Maybe duplicate function assignment using this method is prohibited and checked in same way as duplicate function names?

2. A lazy assignment operator. I'm not sure how the validation of assignments works under the hood, but might it be possible to have an alternate operator for lazy assignment, say ::= or something? I haven't really thought this one out as much, as I thought alternative #1 might be simpler and more parallel with existing syntax. There could be a number of possibly simplifying aspects of such an operator. It's possible that it's not true lazy evaluation, but maybe just bypasses some syntax/identifier checks? Maybe it would be restricted to assigning function objects or callables, so that checks/errors could just be deferred to call time? Maybe it would be restricted only to assignment of constants (depending on how that upcoming implementation will work)? Or some combination of those?

Code: Select all

;===== GLOBAL FUNCTIONS ========================
isOdd ::= neg(isEven)   ;is odd number (proposed "lazy assignment")
isEven ::= isDiv.bind(2)   ;is even number (proposed "lazy assignment")
isDiv(y,x) => mod(x,y) == 0   ;is divisible by
neg(f) => (x*) => !f(x*)   ;negate
In this case, perhaps this order is allowed because the lazy assigment delays the check, for example, of both the existence of the functions and the application of the composition until call time?

These might be ideas you've already considered and discarded. Or perhaps they're just bad ideas or have too narrow a use case. I know functional programming isn't for everyone. However, if there were a way to solve this order of declaration issue for composed functions, it seems like AHK could be actually more FP-friendly than even Python. Order of declaration has always been a thorn in my side, when doing any functional programming of even moderate complexity in Python.

Anyhow, thanks so much for all the help and consideration.

lexikos
Posts: 9554
Joined: 30 Sep 2013, 04:07
Contact:

Re: Preview of changes: scope, function and variable references

Post by lexikos » 27 Mar 2021, 22:07

sirksel wrote:
27 Mar 2021, 00:28
1. A function object assignment operator.
That's just confusing.

With parentheses,
  1. x := isEven() => isDiv.bind(2) assigns x a function named isEven which, when called, makes and returns a bound function.
  2. class { isEven() => isDiv.bind(2) } defines a method which, when called, makes and returns a bound function.
  3. In global scope, isEven() => isDiv.bind(2) defines a function named isEven which, when called, makes and returns a bound function.
Without parentheses,
  1. x := isEven => isDiv.bind(2) assigns x a function which, when called, discards its first parameter (named isEven) and makes and returns a bound function. In this case, isEven is shorthand for (isEven).
  2. class { isEven => isDiv.bind(2) } defines a read-only property with a getter which, when called, makes and returns a bound function. In this case, isEven => ... is shorthand for isEven { get => ... }, which is shorthand for isEven { get { return ... } }.
  3. In global scope, you want isEven => isDiv.bind(2) to make a bound function during script startup and assign it to isEven.
In every case except the last, => isDiv.bind(2) is shorthand for a function body equivalent to { return isDiv.bind(2) }. The expression is evaluated when the function is called (whether that's a plain function, a method, or a property accessor function).

Global variables are to properties what methods are to global functions. In a class body, x => y creates a read-only property with a getter which evaluates y each time and returns the result. If anything, x => y outside of a class body should create a read-only global variable which evaluates y each time, not at script startup. In both cases it would be equivalent to something.DefineProp('x', {get: something => y}), except that something for the global case has no name in the current iteration of the language¹. (This isn't an original concept; in JavaScript, for instance, global variables are literally properties of the global object, which varies depending on the host, such as web browsers, Node.js, ActiveScript.ahk, the ScriptControl COM object, WScript, etc.)

In that case, isOdd => neg(isEven) would create a read-only global variable which returns the result of calling neg. The expression neg(isEven) would not be evaluated until isOdd is retrieved (prior to it being called), but it would be evaluated every time isOdd is retrieved. You would need to memoize it manually. The global equivalent of DefineProp could allow you to replace the current definition of isOdd with the new one, so the getter isn't called more than once.

¹ For a while I've been thinking in terms of module support (thank Helgef for that). If global is a module corresponding to the global namespace, then global.name could be used in expressions to refer to (or assign) a global variable without prior declaration. It might seem natural to extend DefineProp to modules, so global.DefineProp('name', descriptor)² defines a global variable (potentially with accessor functions) or a function, but it wouldn't play well with load-time resolution of global names unless name is required to already exist. (There are alternative solutions, but having already disabled dynamic creation of variables with %%, I'm not going into that right now.)

² I probably won't allow DefineProp to be used in this manner, because I don't think Object methods should be mixed with the module's actual exports. Speaking of which, I suppose the module itself wouldn't actually be equivalent to the global namespace, because one wouldn't necessarily want to export all global variables. In that case, global wouldn't represent the module's public interface; it could be an internal module, but more likely it would be a unique object (or a symbol without an actual value) used for namespace resolution, without the capability to mutate its interface like an Object.

In the same way that the property getter syntax unconditionally defines a unique function object, regardless of position of declaration, could the same or similar syntax be allowed in the global scope?
When is isDiv.bind(2) supposed to be evaluated? It's unclear. This is one of the reasons I say you are better of composing functions the normal way. If you define isEven as a function, the functions it calls only need to be initialized some time prior to isEven being called.

Maybe this might even solve the issue of availability of composed functions in objects' static __New code, since it might be possible for these function assignments to be evaluated similarly or at the same time as the global functions themselves?
That would mean isDiv.bind(2) must be evaluated prior to initialization of classes, which is another rule to remember. And what if a class initializer wants to use it?

Global functions aren't evaluated at load time; they are constructed from parsing the source code and assigned to constants, in the same manner that variables names are resolved to addresses, or expressions are parsed and tokenized (but not evaluated). Local functions are constructed at the same time, although closures are instantiated when the outer function is called. This all works to allow them to be non-positional, only because it's done before evaluating any script code, such as isDiv.bind(2).

too narrow a use case
And too esoteric, I think.

I was originally hoping I could refactor to a more functional composition style to take fuller advantage of the new (and most excellent!) streamlined function object syntax.
I'm not seeing the advantage in this case, but maybe your examples are just too simple.

QiuDao
Posts: 18
Joined: 25 Mar 2021, 22:55

Re: Preview of changes: scope, function and variable references

Post by QiuDao » 07 Apr 2021, 00:07

lexikos wrote:
11 Mar 2021, 22:27
Shouldn't use

Code: Select all

a:=(value.methodname)
instead of

Code: Select all

a:=Value.GetMethod(methodName)
?

Post Reply

Return to “AutoHotkey Development”