This post is based on the true story with minor changes for readability.
Let's say you're on interview for frontend developer position. Interviewer asks you to write a function to add two numbers.
That's easy and you come up with
function add(a, b) {
return a + b;
}
Next you're asked to modify the function to the add(1)(2)
syntax.
Well,
function add(a) {
return function(b) {
return a + b;
}
}
More parentheses! add(1)(2)(3)
should return 6.
No problem:
function add(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
So far so good. Next task is to write add
function with the requirements:
+add(1)(2) // should return 3
+add(1)(2)(3) // should return 6
It's clear that the previous solution should be rewritten to accept any amount of parentheses. We also notice the plus sign before the add
function which leads us to think about type coercion.
What if we always return a function from add
and coerce it to a primitive number when necessary? JavaScript calls valueOf method to convert a function to a primitive number.
function add(a) {
return add;
}
add.valueOf = function() {
return 42;
}
console.log(+add(1)(2)); // 42
console.log(+add(1)(2)(3)); // 42
We return function add
from the function add
and overwrite it's valueOf
method to return a constant number when coerced to a primitive.
We don't get the correct result yet, but the big step is made. We don't get a runtime error and are able to return a number! The next step is to correctly sum the numbers.
Somehow we should accumulate the arguments the add
function was called with. Let's start the easiest way with counter.
let counter = 0;
function add(a) {
counter += a;
return add;
}
add.valueOf = function() {
return counter;
};
console.log('Should be 3', +add(1)(2)); // 3
console.log('Should be 6', +add(1)(2)(3)); // 9
The first result is correct, but the second is wrong, because the counter was not resetted after the first coercion. Let's fix this.
let counter = 0;
function add(a) {
counter += a;
return add;
}
add.valueOf = function() {
let temp = counter;
counter = 0;
return temp;
};
console.clear();
console.log('Should be 3', +add(1)(2)); // 3
console.log('Should be 6', +add(1)(2)(3)); // 6
Great! Now everything works as expected! But the code isn't great, we can do better. Let's refactor ๐
function add(a) {
let counter = a;
function inner(b) {
counter += b;
return inner;
}
inner.valueOf = () => counter;
return inner;
}
console.log('Should be 3', +add(1)(2)); // 3
console.log('Should be 6', +add(1)(2)(3)); // 6
Awesome! โจ The result is correct and the code is nice. We created function inner
inside the add
and return it. The counter
variable is in closure and there is no need to reset it like in the previous example.
Now it's possible to write expressions like this:
let result = add(1)(2) + add(1)(2)(3) + add(1)(2)(3)(4) + add(1)(2)(3)(4)(5);
console.log(result); // 34
And get the correct result.
What do you think about such tasks on interviews? What questions were you asked on an interview? Please share in the comments!
Top comments (9)
Thanks for the fun post Eugene.
After reading your post,
I was playing around to implement it with Function#bind but it seems like it's not possible to know when user is "done" with arguments and calculate the "add" without knowing the "arity" (parameter count).
I ended up using "done" as an end condition but one can probably specify the arity initially as a second argument of
curriedAdd(arity, value)
and compare the arity against the argument length.But then at this point, it will be a partial application, not a currying...
And then I was surprised ๐ฎ to learn from your post that the type-coercion using
+
causes.valueOf
to be called..
So I've tried for the 3rd time to make the function work as yours do using
.valueOf
.This post got me really thinking about currying and steps to reach your conclusion as well as mine.
Below is the source above.
Wow, thanks for the such detailed comment! I did not think about
bind
ing and collecting arguments with each function call. JS is so flexible language that allows many ways to solve a problem. Thanks for sharing your path!I found your approach your readable as it is more intentional what the code is doing ๐
Honestly I think this kind of question isn't suitable for an interview and is an "edge case" of programming knowledge. You'd never write code like this in the real world, therefore it has no commercial value and seems daft to test in an interview.
In my opinion interview questions/tasks should be "real world" and allow the candidate to show their critical thinking and problem solving abilities, rather than testing their understanding of language nuances.
Well, I was not happy with this task as well. It took me about 40 minutes to solve it ๐
Other tasks were closer to the problems a programmer solves in his/her day-to-day work.
But it was very interesting for me to solve this task and it was the inspiration for this post.
Yeah - you did a great job to be able to solve it!
Nice one! I agree this is a very weird interview question to be asking, but since we're already doing crazy stuff, you inspired me to go further. This version can accept multiple numbers or callbacks at once, and will add/apply all to the count.
Thanks for sharing, I like how you go deeper with
add
functionality.Nice info...sometimes we need to test fundamental things about the language...its essential as people today are more familiar with frameworks