Repository navigation
Details of mathjs function closures and bound and free variables #3724
gwhitney
started this conversation in
Design decisions
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
This posting has two parts. In the first, I detail some aspects of how mathjs expression language function closures work, to surface them and make sure they are intentional design decisions.
Second, I raise what was to me a surprise in the behavior of bound variables, which has some interaction with the first point.
1: In slogan form, function definitions close over variables bound in a containing syntactic environment, but not unbound variables.
To see this behavior, evaluate
g(y) = f(x) = y*x +k(with a so-called "scope" that has a binding fork, say set to 3). Then evaluateF=g(10)andH=g(-2). You can then check thatF(5)returns 53, andH(5)returns -7 = -2*5 + 3. Now setkto 7 in the scope provided when the expression defininggwas evaluated. Then check thatF(5)returns57andH(5)returns -3. You can also check that any value thatymight have in the scope provided whengwas defined has no effect on the behavior ofForH. To summarize, the execution of each functionfreturned byguses the current ("dynamic") value ofkfrom the scope in whichgwas defined, but uses the ("lexical")yvalue in effect withingwhen it is called and that instance offis defined.These scoping rules may be exactly as designed. The mathjs documentation explicitly states that function definitions do not close over the values of free variables, but it is silent on the behavior with respect to variables bound in enclosing syntactic environments. And certainly the lexical scoping for
yis useful, as it allows one to write function factories within mathjs (e.g., the "addn" function that given n, returns a function that adds n to its argument). So perhaps the documentation of function definitions should be elaborated to note this behavior.2: Bound variables are not writable. To see this, try
f(x) = (x=x+1) > 2 ? x : x.aand then evaluatef(2). It returns 2, even though thex=x+1must have been evaluated before thexin the true part (as proven because the illegalx.ain the false part does not error, or because evaluatingk = 2; (k=k+1) > 2 ? k : k.aproduces 3). If x were writable,f(2)should have returned 3.This limitation on bound variables prevents one from using a closure over outer bound variables to generate functions with "private variables," e.g., you can't write a counter generator as
countergen(y) = (y=0) == 0 ? counter() = (y=y+1) : undefinedIs it intentional that bound variables cannot be assigned to? (If they happen to be objects, they can be mutated, so the following counter generator works:
countergen(y) = (y.cnt = 0) == 0 ? counter() = (y.cnt = y.cnt + 1) : undefinedas long as you call countergen with an object, e.g.ctr = countergen({})lets you callctr()repeatedly and get 1, 2, 3, ...)If not, then that's a bug that should be filed as an issue, to allow bound variables to be reassigned.
If it is the intentional designed behavior, then it seems to me that at least, the read-only nature of bound variables should be documented, but more, it seems to me that attempts to assign to a bound variable should throw an error, to avoid code that appears to be calculating with bound variables but is not. If there's agreement on that, then I will file an issue to implement such an error.
Looking forward to thoughts/responses.
All reactions