Default parameters and currying: Python vs. Javascript

779 views Asked by At

Consider the following problem: I want to create an array of functions, each function just prints its index in that array. In Python, it can be done easily with

funcs = []
for i in range(5):
   funcs.append(lambda i=i: print(i))
funcs[2]()
# 2

Here we use default argument values as a way to do currying (if I understand the term right).

Prior to ES6, there were no default argument values in Javascript, so currying had to be done in different way. Now we have them and I tried to translate Python to Javascript literally:

funcs = []
for (var i=0; i<5; i++) {
   funcs.push(function (i=i) {console.log(i)})
}
# this part pass OK
funcs[2]()
ReferenceError: i is not defined
    at Array.<anonymous> (evalmachine.<anonymous>:3:27)
    at evalmachine.<anonymous>:1:9
    at ContextifyScript.Script.runInThisContext (vm.js:26:33)
    at Object.exports.runInThisContext (vm.js:79:17)
    at run ([eval]:608:19)
    at onRunRequest ([eval]:379:22)
    at onMessage ([eval]:347:17)
    at emitTwo (events.js:106:13)
    at process.emit (events.js:191:7)
    at process.nextTick (internal/child_process.js:752:12)

Why it fails? What's the difference between Python and Javascript ways to pass default values?

(Okay, I know that I can use let here instead of var, I'm just studying Javascript after several years with Python and trying to understand it underhoods.)

3

There are 3 answers

2
juanpa.arrivillaga On BEST ANSWER

Both Javascript and Python using late-binding in closures. However, using a default argument is a hack that let's you simulate early binding in Python, and that works because default parameters are evaluated at function definition time in Python. However, in Javascript default arguments, according to the docs

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters

The default argument gets evaluated at call time, so unlike e.g. in Python, a new object is created each time the function is called.

Here is one of the solutions used in Python that can be applied to this problem in Javascript. I've transliterated into Javascript the best I could. Essentially, define another anonymous function that returns your original, and apply it at the same time. This is very messy to my eyes, and in Python, I always go with the default-argument:

funcs = []
for (var i = 0; i < 5; i++) {
    funcs.push((function (i) {return function() {console.log(i)}})(i))
};
funcs[0]() // 0
funcs[4]() // 4

In Python:

>>> funcs = []
>>> for i in range(5):
...     funcs.append((lambda i: lambda : print(i))())
...
>>> funcs[0]()
0
>>> funcs[4]()
4

I think it is clear that you should use .bind in Javascript, as elucidated in other answers, and not this clunky solution.

1
AdamW On

If you use let instead of var you get a new binding for each iteration.

funcs = []
for (let i=0; i<5; i++) {
  funcs.push(function (j=i) {console.log(j)})
}

funcs[2];//2

i=i doesn't work because in ES6 parameters can define defaults using other parameters

f = function(a=1, b=a){console.log(b);}
f() // 1

So the parser is getting confused.

4
mhodges On

Your issues have to do with the difference between when default parameters get bound in closures in python vs JavaScript. While it is true that both JavaScript and Python use late-binding, in the case of default parameters, Python simulates early-binding, whereas JavaScript does not.

That being said, if you are going to be creating closures like this, you may as well take advantage of them and ditch the parameters all together, honestly.

You mentioned the use of let and that is important if you want to define the function inside the for loop because otherwise funcs[n] will always be your maximum value of your iterator (due to the late-binding of JavaScript closures).

Try this:

funcs = [];
for (let i=0; i<5; i++) {
   funcs.push(function () {console.log(i)});
}
funcs[2]();

Alternatively, if you want to follow good practice of not defining functions within a loop, you can define the function outside, and pass the variable in using .bind(). One thing to note is that this method will bind the variable with the value at the time .bind() is called, so you do not have to use let

funcs = [];
function myFunc(i) {
  console.log(i);
}
for (var i=0; i<5; i++) {
   funcs.push(myFunc.bind(this, i));
}
funcs[2]();