DEV Community

Cover image for Interview Question Journey - Currying, Closures, Type Coercion, oh my ๐Ÿ˜ฑ
Eugene Karataev
Eugene Karataev

Posted on

Interview Question Journey - Currying, Closures, Type Coercion, oh my ๐Ÿ˜ฑ

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)

Collapse
 
dance2die profile image
Sung M. Kim • Edited

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.

using bind

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.

coercion.

So I've tried for the 3rd time to make the function work as yours do using .valueOf.

3rd try

This post got me really thinking about currying and steps to reach your conclusion as well as mine.

Below is the source above.

function curriedAdd(v1) {
    if (arguments[arguments.length - 1] === "done") {
        console.log(`done...`)
        return Array.prototype.slice
            .apply(arguments, [0, arguments.length - 1])
            .reduce((acc, v) => acc+=v)
    };
    console.log(`v1`, v1, `arguments`, ...arguments, 'arg.length', arguments.length)
    return curriedAdd.bind(null, ...arguments);
}

function curriedAdd2(arity, v1) {
    // "-1" to account for the arity
    if (arguments.length - 1 === arity) {
        return Array.prototype.slice
            .call(arguments, 1)
            .reduce((acc, v) => acc += v, 0)
    };
    return curriedAdd2.bind(null, ...arguments);
}


function curriedAdd3() {
    const inner = curriedAdd3.bind(null, ...arguments)
    inner.valueOf = () => [...arguments].reduce((acc, v) => acc += v, 0);
    return inner;
}

Collapse
 
karataev profile image
Eugene Karataev

Wow, thanks for the such detailed comment! I did not think about binding 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!

Collapse
 
dance2die profile image
Sung M. Kim

I found your approach your readable as it is more intentional what the code is doing ๐Ÿ™‚

Collapse
 
joshbrw profile image
Josh Brown

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.

Collapse
 
karataev profile image
Eugene Karataev

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.

Collapse
 
joshbrw profile image
Josh Brown

Yeah - you did a great job to be able to solve it!

Collapse
 
minimumviableperson profile image
Nicolas Marcora

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.

const add = (...args) => {
    let count = 0

    const applyArguments = args => args.reduce((acc, arg) => {
        if (typeof arg === 'function') return arg(acc)
        if (typeof arg === 'number') return acc + arg
        return acc
    }, count)

    const adder = (...args) => args[0] === undefined
        ? count
        : (count = applyArguments(args), adder)

    adder.valueOf = () => count

    return adder(...args)
}


const double = n => n * 2

const timesTen = n => n * 10

add(1, 2, 3)(double)(10)(timesTen, double)(2)()
// 442

+add(1, 2, 3)(double)(10)(timesTen, double)(2)
// 442

add(10)(double) + add(20)(timesTen)
// 220
Collapse
 
karataev profile image
Eugene Karataev

Thanks for sharing, I like how you go deeper with add functionality.

That escalated quickly

Collapse
 
budyk profile image
Budy • Edited

Nice info...sometimes we need to test fundamental things about the language...its essential as people today are more familiar with frameworks