I've been working on a project lately that involves writing a lot of callback functions, and some of them were getting pretty large. I decided to move as many of them as possible to separate modules to keep things small and relatively simple, but noticed that a lot of them depended on the parent function's scope. Functions in Javascript can't inherit the scope of something that they can't see. The easy way to fix a problem like this would just be to write a function that accepts the scope variables needed and then returns the callback function. This works because the returned callback function will inherit the scope of the parent (the passed variables). So something like this:
const helloCallback = (instanceArg1, instanceArg2) =>
(callbackArg1, callbackArg2) => {
// Both instance args and callback args are in scope here!
}
export default helloCallback
would be called like this in a listener in a separate module:
import helloCallback from './helloCallback'
pretendAPI.onMessage(helloCallback(arg1, arg2))
It's a pretty simple and elegant solution! In the functional programming world, we call something like this currying (using this very loosely). True currying is where you split all of your function arguments over consecutive functions like Russian dolls. It looks like this:
const helloCallback = instanceArg1 => instanceArg2 => callbackArg1 => callbackArg2 => {
// all the variables are in scope!
}
This wasn't really optimal for what I needed though, so I just split my function over two different levels.
For the hell of it, I decided to write my own function that would automatically curry any function. It would be used like this:
const curried = curry((one, two, three, four) => console.log(one, two, three, four))
and could be called in any of these ways:
curried(1)(2)(3)(4)
// Output: 1 2 3 4
curried(1, 2)(3, 4)
// Output: 1 2 3 4
curried(1, 2, 3, 4)
// Output: 1 2 3 4
And did I mention that it's only 8 lines long? Let's see how I wrote it.
There's a few pieces of information we need to know before we're able to write this curry
function. First, what the hell is going on here? If we look at how the function is used, we can see that curry
accepts in a function and then returns another function. There's an important thing to note here: the function that's returned is not the same as what we passed in. The function that's returned will either return the value of our original function with all of the arguments somehow magically applied, or it will return another function that accepts more arguments in. It might not be immediately obvious at first, but there's some kind of recursion going on in the curry
function because we're returning a different number of functions depending on the inputs to each previous function.
With this in mind, we can start writing the skeleton of the curry
function:
const curry = functionToCall => {
const recursiveSomething = () => something => {
if (someCondition) return functionToCall(someArgs)
else return recursiveSomething()
}
return recursiveSomething()
}
Let's look at this line by line. Our curry
function accepts in an argument called functionToCall
that we'll eventually call (great naming, amiright?). Then on the next line, we define a recursive function that returns another function. The function name is just used here so that we're able to recursively return functions as needed; as far as I know, it isn't possible to return anonymous functions that can be called recursively in Javascript. The returned function accepts in some arguments, and depending on someCondition
we'll either return functionToCall
with some arguments passed down to it or we'll return the results of a call to recursiveSomething
, which is the function we're currently in. Last, we call recursiveSomething
, returning our conditional-return function mess.
This may look pretty complicated, but we've actually got half of the function written. All that's left to do is fill in the blanks. The main problem we're trying to solve here is argument storage: we need some place to put all of the arguments we're going to receive so that we can pass it down to our "callback function" in one go. The easiest way to do this is to just use a rest parameter, an array to store all of the arguments in, and then just spread that array over the functionToCall
's arguments when we call it:
const curry = functionToCall => {
let argumentsArray = []
const recursiveSomething = () => (...args) => {
argumentsArray = argumentsArray.concat(args)
if (someCondition) return functionToCall(...argumentsArray)
else return recursiveSomething()
}
return recursiveSomething()
}
Going through the lines we added, we can see that we added an array argumentsArray
that's outside of the recursiveSomething
function. This is important because it's in the scope of not only the root recursiveSomething
return function, but all future returned functions. In the return function, we added a rest parameter (allows our function to accept in unlimited arguments and puts them in an array), and then concatenated that with the argumentsArray
. Last, we used spread syntax to apply the arguments in the array to functionToCall
when we call it.
This is great, we're actually really close to finishing our auto-curryer! We just need to fill in when we'll call functionToCall
, or the base case for our recursive function. We want to call functionToCall
if and only if we have all of the arguments we need to actually call it. Functions in Javascript have a length property, so we can use this to check whether the length of argumentsArray
is equal to the number of arguments expected by the function:
const curry = functionToCall => {
let argumentsArray = []
const recursiveSomething = () => (...args) => {
argumentsArray = argumentsArray.concat(args)
if (argumentsArray.length === functionToCall.length) return functionToCall(...argumentsArray)
else return recursiveSomething()
}
return recursiveSomething()
}
And that's it! We can now pass curry
a function and it'll automatically curry all of the arguments for us thanks to the magic of recursion! Not bad for only eight lines. If you want, you can also add a few more checks to support zero argument functions and to make sure you call the function correctly:
const curry = functionToCall => {
if (functionToCall.length === 0) return functionToCall;
let argumentsArray = [];
const recursiveSomething = () => (...args) => {
if (
(args.length === 1 && argumentsArray.length + 1 > functionToCall.length) ||
(argumentsArray.length === 0 && args.length > functionToCall.length) ||
args.length + argumentsArray.length > functionToCall.length
)
throw new Error("Wrong number of arguments received");
argumentsArray = argumentsArray.concat(args);
if (argumentsArray.length === functionToCall.length) return toCall(...argumentsArray);
return recursiveSomething();
};
return recursiveSomething();
};
Top comments (0)