Challenge – Function that only runs with no arguments (intermediate / advanced functional programming)

This is a functional programming challenge I had to solve in an interview. I found it very interesting and it had me thinking for a while. Now, a few years later, I suddenly remembered it, so I thought I’d post about it.

If you’re interested in a brain teaser, have a go. It’s an intermediate / advanced functional programming challenge.

(If you don’t know a lot about functional programming and want to look at that first, then check out functional programming – the ultimate beginner’s guide.)

The challenge

There were two parts to the challenge. Part 1 and part 2. Each part increases in difficulty.

I’ve also added a part 3.

Challenge, part 1

Design a function that you can keep calling in a chain as long as you keep passing in arguments. During this time, it shouldn’t do anything except collect the arguments. Finally, when you call the function without an argument, it should call a real function, such as console.log, with the arguments you’ve passed in so far.

Here is a code example showing how this function should behave:

foo('a'); // nothing happens
foo('a')('b'); // nothing happens

foo(); // logs an empty line (calls console.log())
foo('a')(); // logs 'a' (calls console.log('a'))
foo('a')('b')() // logs 'a b' (calls console.log('a', 'b'))
foo('a')('b')('c')() // logs 'a b c' (calls console.log('a', 'b', 'c'))'

// optionally, the function can accept more than one argument at once
foo('a', 'b')() // same as foo('a')('b')()
foo('a')('b', 'c')() // same as foo('a')('b')('c')()

Challenge, part 2

Implement a function similar to question 1, but make it "stateless"?

Different function calls shouldn’t share state with each other.

For example:

const a = foo('a');
const ab = a('b');
const abc = ab('c');

a(); // logs 'a' (calls console.log('a'))
abc(); // logs 'a b c' (calls console.log('a', 'b', 'c'))
ab(); // logs 'a b' (calls console.log('a', 'b'))

The difference compared to part 1, is that calling a('b') shouldn’t also add the argument 'b' to the call for a(). a() should log 'a', not 'a b'.

Challenge, part 3

Make a function similar to the one for part 2, but the first time it’s called it must be called with a single argument. That argument should be the function it will run in the end.

Then, as normal, the function should keep accepting and collecting arguments. When it’s finally called without an argument, it should execute the function you passed in as the first argument with the arguments it has collected.

For example:

const log = actWhenCalledWithoutArguments(console.log);
const a = log('a');
const ab = a('b');
const abc = ab('c');

a(); // logs 'a' (calls console.log('a'))
abc(); // logs 'a b c' (calls console.log('a', 'b', 'c'))
ab(); // logs 'a b' (calls console.log('a', 'b'))

Hints – Part 1

So what would your solution to these questions be? Before reading on, try solving them on your own.

If you need help, read on for some hints.

Hint 1 – Currying

For the first hint, notice that these functions sound a lot like currying. Curried functions also accept arguments multiple times, before finally executing the "real functionality" with all of the arguments they’ve collected.

In particular, the curry utility seems very relevant. That’s a function that takes a normal function, which isn’t curried, and turns it into a curried function.

It seems relevant because unlike a normal curried function, the curry utility has to work with a variable number of arguments.

So, you may want to start by implementing the curry utility first. Looking at its source code may help you figure out the solution for the challenges.

Feel free to try implementing it yourself.

Otherwise, here is an example implementation:

function curry(fn, arity = fn.length) {
  function execute(...args) {
    if (args.length === arity) {
      return fn(...args);
    }
    function gatherMoreArgs(...moreArgs) {
      return execute(...args, ...moreArgs);
    }
    return gatherMoreArgs;
  }
  return execute;
}

Hint 2 – More functions

For the challenges, you don’t need to have just one top-level function. Just like the curry utility, you can have more functions. The curry utility itself is made up of 3 nested functions in total.

If you’re stuck on your solution, explore what you can do with 1 function, 2 functions or even 3 functions, nested or not.

Hint 3 – How to gather arguments for part 1

This hint is more spoiler-heavy. It talks about a possible implementation for part 1.

One way to gather the arguments is to have an array. Then, every time the function is called with arguments, push them to that array.

Solution – Part 1

Alright, if you’re ready, here’s the solution for part 1:

const logWhenCalledWithoutArguments = (...args) => {
  const argsSoFar = [];

  const execute = (...moreArgs) => {
    if (moreArgs.length === 0) {
      console.log(...argsSoFar);
    } else {
      argsSoFar.push(...moreArgs);
      return execute;
    }
  }

  return execute(...args);
}

logWhenCalledWithoutArguments is a higher order function (a function that returns another function). This is needed so that it can create a private array (argsSoFar), that exists only for that invocation of logWhenCalledWithoutArguments. That way, different calls of logWhenCalledWithoutArguments won’t share the same arguments.

Other than that, the execute function does the real work. First, it checks whether it was called without arguments. If that’s the case, then it calls console.log with all of the arguments collected so far. Otherwise, it adds the new arguments to argsSoFar. Then, it returns itself so that the user can call the function again and keep providing arguments.

At the end of logWhenCalledWithoutArguments, we immediately call the execute function and return its result. That way, we start checking and collecting arguments from the first call.

Hint – Part 2 – Making it stateless

This is another spoiler hint.

Part 2 of the solution can’t share state like part 1 did. With the solution for part 1, this code wouldn’t work:

const a = logWhenCalledWithNoArguments('a');
const ab = a('b');
a(); // incorrectly logs 'a b'
ab(); // correctly logs 'a b'

So you can’t use a shared array like in part 1.

Instead, you have to find a way to pass in the arguments across different calls using closures.

Solution – Part 2

Alright, if you’re ready, here’s the solution to part 2.

const logWhenCalledWithoutArguments = (...firstArgs) => {
  const gatherArgs = (...args) => {
    const execute = (...moreArgs) => {
      if (moreArgs.length === 0) {
        return console.log(...args);
      }
      return gatherArgs(...args, ...moreArgs);
    }
    return execute;
  }
  const firstExecute = gatherArgs();
  return firstExecute(...firstArgs);
}

This function has a lot of similarities to the solution for part 1. The main difference is that, instead of capturing arguments in an array, we use the function gatherArgs to hold the arguments.

Also, just like before, we run the execute function straight away, to check if we got an argument on the first call.

Solution – Part 3

For part 3, the first call must be a single argument, the function to execute at the end. Everything else is the same.

If you’re ready, here’s the solution:

const actWhenCalledWithoutArguments = (fn) => {
  const gatherArgs = (...args) => {
    const execute = (...moreArgs) => {
      if (moreArgs.length === 0) {
        return fn(...args);
      }
      return gatherArgs(...args, ...moreArgs);
    }
    return execute;
  }
  return gatherArgs();
}

The solution is very similar to the solution for part 2.

The main difference is that we don’t need to check for an argument on the first call. As a result, we don’t need to run execute straight away. All we have to do is return it.

The other difference is that we replace the call to console.log with fn (the first argument).

Other than that, everything else is the same.

Final notes

Anyway, that’s it for this challenge.

I hope you enjoyed it.

Let me know if you completed, and whether you thought it was easy or difficult, in the comments. It’s not the kind of thing you have to implement at work every day.

Also, if you have any similar challenges, or if you have any tips or feedback for this one, then please leave a comment.

Alright, see you next time.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments