This is an intermediate / advanced functional programming challenge I had to solve in an interview. I found it very interesting and it had me thinking for a while.
If you're interested in a brain teaser, have a go. You can also complete it using classes and methods if you prefer, but we'll use functions in this article.
(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 small part 3.
Challenge, part 1
Design a function that you can keep calling in a chain as long as you keep passing in arguments. For example, foo(argument1)(argument2)
. 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');
log(); // logs an empty line (calls console.log())
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. This is very similar to what you're trying to do in this challenge.
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. In its implementation, it has a mechanism for storing arguments and then calling a function with all of the stored arguments later.
So, you may want to start by implementing the curry
utility first. Looking at its source code may be helpful for the challenges.
Feel free to try to implement 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;
}
// example usage
function foo(a, b, c, d) {
console.log(a, b, c, d);
}
const curriedFoo = curry(foo, 3);
curriedFoo('a')('b', 'c')('d'); // logs 'a b c d' (console.logs('a', 'b', 'c', 'd'))
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 it, 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.
Top comments (0)