Search code examples
javascriptserializationclosurescurryingarrow-functions

How to correctly serialize Javascript curried arrow functions?


const makeIncrementer = s=>a=>a+s
makeIncrementer(10).toString()    // Prints 'a=>a+s'

which would make it impossible to de-serialize correctly (I would expect something like a=>a+10 instead. Is there a way to do it right?


Solution

  • Combining ideas from the two answers so far, I managed to produce something that works (though I haven't tested it thoroughly):

    const removeParentheses = s => {
        let match = /^\((.*)\)$/.exec(s.trim());
        return match ? match[1] : s;
    }
    
    function serializable(fn, boundArgs = {}) {
        if (typeof fn !== 'function') return fn;
        if (fn.toJSON !== undefined) return fn;
    
        const definition = fn.toString();
        const argNames = removeParentheses(definition.split('=>', 1)[0]).split(',').map(s => s.trim());
    
        let wrapper = (...args) => {
            const r = fn(...args);
    
            if (typeof r === "function") {
                let boundArgsFor_r = Object.assign({}, boundArgs);
                argNames.forEach((name, i) => {
                    boundArgsFor_r[name] = serializable(args[i]);
                });
                return serializable(r, boundArgsFor_r);
            }
            return r;
        }
    
        wrapper.toJSON = function () {
            return { function: { body: definition, bound: boundArgs } };
        }
        return wrapper;
    }
    
    const add = m => m1 => n => m + n * m1,
        fn = serializable(add)(10)(20);
    
    let ser1, ser2;
    
    console.log(
        ser1 = JSON.stringify(fn)          // {"function":{"body":"n => m + n * m1","bound":{"m":10,"m1":20}}}
    );
    
    const map = fn => xs => xs.map(fn),
        g = serializable(map)(n => n + 1);
    
    console.log(
        ser2 = JSON.stringify(g)   // {"function":{"body":"xs => xs.map(fn)","bound":{"fn":{"function":{"body":"n => n + 1","bound":{}}}}}}
    );
    
    const reviver = (key, value) => {
        if (typeof value === 'object' && 'function' in value) {
            const f = value.function;
            return eval(`({${Object.keys(f.bound).join(',')}}) => (${f.body})`)(f.bound);
        }
        return value;
    }
    
    const rev1 = JSON.parse(ser1, reviver);
    console.log(rev1(5));   // 110
    
    const rev2 = JSON.parse(ser2, reviver);
    console.log(rev2([1, 2, 3]));   // [2, 3, 4]
    

    This works for arrow functions, that do not have default initializers for the arguments. It supports higher order functions as well. One still has to be able to wrap the original function into serializable before applying it to any arguments though. Thank you @MattWay and @ftor for valuable input !