Tuesday 17 September 2024

Function Currying Revisited (fixed)

For whatever the reason some days ago I was looking into adding a curry function to a python micro-library with some utility functions that I use in some small projects. I took a look to this post that I wrote some years ago, and I realized that my javascript implementation was wrong. I'll start by writing it again with the same logic, but supporting calls with several parameters at one time, that was not supported in that old version.



//This one is WRONG
function buggyCurry(originalFunc) {
    let providedArgs = [];
    return function curriedFn(...args) {
        providedArgs.push(...args);
        return providedArgs.length >= originalFunc.length
            ? originalFunc(...providedArgs)
            : curriedFn;
    };
}


Well, the idea is that to create a curried version of a provided function we have to create a function with state, where that state is the original function and the list of already provided parameters. As we invoke the curried function these parameters are appended to that list and the same curried function is returned. Once we have provided all the paremeters the original function is invoked and its return value returned. OK, but the above implementation is wrong. Keeping the list of provided parameters as state of the curried function means that when we start another round of calls to the curried function (a "curried-flow"), those parameters are already there and it fails. I mean:


function formatMessages(msg1, msg2, msg3) {
    console.log(`${msg1}-${msg2}-${msg3}`);
}

let curriedFormat = buggyCurry(formatMessages);

try {
    curriedFormat("a")("b")("c");
    curriedFormat("d")("e")("f");
}
catch (ex) {
    console.log(`Error: ${ex}`);
}
// a-b-c
// a-b-c
// c:\@MyProjects\MyJavaScriptPlayground\currying\curry.js:35
// curriedFormat("d")("e")("f");
//TypeError: curriedFormat(...) is not a function

The first invocation is fine. But the second invocation fails, cause rather than starting again with an empty list of parameters it already starts with those of the previous execution.

To fix that, each time we start a new invocation of the curried function (a "curried-flow") we have to start with a new parameters list. We have to separate the curried function, from the function that stores the state and returns itself until all parameters are provided (the curry logic). The function that starts the "curried-flow" is a sort of bootstrapper or factory. This is what I mean:



//This one is better, but still not fully correct
function curry(originalFunc) {
    // no difference between using an arrow function or a named function expressions, save that the named expression makes clear to me that it's a bootstrapper (or factory)
    //return (...args) => {
    return function curryBootStrap(...args) {
        let providedArgs = [];
        //II(named)FE
        return (function curriedFn(...args) {
            providedArgs.push(...args);
            return providedArgs.length >= originalFunc.length
                ? originalFunc(...providedArgs)
                : curriedFn;
        })(...args);
    };
}


function formatMessages(msg1, msg2, msg3) {
    console.log(`${msg1}-${msg2}-${msg3}`);
}

let curriedFormat = curry(formatMessages);
console.log(curriedFormat.name);

curriedFormat("a")("b")("c");
curriedFormat("d")("e")("f");
curriedFormat("g", "h")("i");
curriedFormat("j", "k", "l");

//a-b-c
//d-e-f
//g-h-i
//j-k-l

That (apparently) works!. But indeed I've just realised that it's not fully correct. It works fine in that use case where we only invoke multiple times the initial function, but it could be that we want to store a reference to some of the intermediate functions and then invoke it multiple times. In that case we would face the same problem as with the first version. That explains whay when checking some articles I always came across with a different implementation that seemed less natural to me, but that is the good one. Based on what I've read here (excellent article where he also talks about the bug that I've just shown), I've implemented it "my way":



// THIS ONE IS CORRECT
function curry(fn) {
    function saveArgs(previousArgs) {
        return (...args) => {
            const newArgs = [...previousArgs, ...args];
            return newArgs.length >= fn.length 
                ? fn(...newArgs)
                : saveArgs(newArgs);
        };
    }
    // Start with no arguments passed.
    return saveArgs([]);
}

In this version, on each invocation of the "curried-flow" either we have reached the end and the original function is invoked, or a new function that traps the existing parameters and joins them in a new array with the ones that it receives, and contains this "curry-logic", is created. The creation of that function is done through another function that I've called saveArgs (the original article named it accumulator). saveAs receives the existing parameters, allowing its child function to trap them. Notice that saveArgs is not a recursive function (neither direct nor indirect), the next invocation to saveArgs is done from a different function (the child function) that is called from the next "curried-flow" invocation, so these calls do not get stacked.

In the above implementations I'm using named function expressions in several places where I could use an arrow function. I'm aware of the differences between normal functions and arrow functions (dynamic "this" vs lexical "this"), that in these cases do not play any role as none of those functions is making use of "this". Using a named function is useful to me cause I try to use semantic names (it's sort of like adding a comment). Additionally there is some trendy use of arrows for factories of other functions that I don't find particularly appealing. I mean, rather than this compact version:


let formatFactory = (pre, pos) => (msg) => `${pre}${msg}${pos}`;

let format1 = formatFactory("-- ", " ..");
console.log(format1("name"));


I prefer writing this more verbose version:


function formatFactory(pre, pos) {
    return (msg) => `${pre}${msg}${pos}`;
}
let format = formatFactory2("-- ", " ..");
console.log(format("name"));

I don't write much JavaScript these days, and I was getting some problems cause I was mixing Kotlin behaviour with JavaScript behaviour. In Kotlin lambdas, if you don't explicitly return a value using the qualified return syntax, the value of the last expression is implicitly returned. This is so regardless of how many expression, declarations or statements it contains. In JavaScript, arrow functions only have an implicit return when they are made up of just one expression.

No comments:

Post a Comment