How to correctly serialize Javascript curried arrow functions?

懵懂的女人 提交于 2021-01-27 06:20:11

问题


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?


回答1:


This is a great question. While I don't have a perfect answer, one way you could get details about the argument/s is to create a builder function that stores the necessary details for you. Unfortunately I can't figure out a way to know which internal variables relate to which values. If I figure out anything else i'll update:

const makeIncrementer = s => a => a + s
const builder = (fn, ...args) => {
  return {
    args,
    curry: fn(...args)
  }  
}
var inc = builder(makeIncrementer, 10)
console.log(inc) // logs args and function details
console.log(inc.curry(5)) // 15

UPDATE: It will be a mammoth task, but I realised, that if you expand on the builder idea above, you could write/use a function string parser, that could take the given args, and the outer function, and rewrite the log to a serialised version. I have a demo below, but it will not work in real use cases!. I have done a simple string find/replace, while you will need to use an actual function parser to replace correctly. This is just an example of how you could do it. Note that I also used two incrementer variables just to show how to do multiples.

function replaceAll(str, find, replace) {
  return str.replace(new RegExp(find, 'g'), replace)
}

const makeIncrementer = (a, b) => c => c + a + b
const builder = (fn, ...args) => {
  // get the outer function argument list
  var outers = fn.toString().split('=>')[0]
  // remove potential brackets and spaces
  outers = outers.replace(/\(|\)/g,'').split(',').map(i => i.trim())
  // relate the args to the values
  var relations = outers.map((name, i) => ({ name, value: args[i] }))
  // create the curry
  var curry = fn(...args)
  // attempt to replace the string rep variables with their true values
  // NOTE: **this is a simplistic example and will break easily**
  var serialised = curry.toString()
  relations.forEach(r => serialised = replaceAll(serialised, r.name, r.value))
  return {
    relations,
    serialised,
    curry: fn(...args)
  }  
}
var inc = builder(makeIncrementer, 10, 5)
console.log(inc) // shows args, serialised function, and curry
console.log(inc.curry(4)) // 19



回答2:


You shouldn't serialize/parse function bodies since this quickly leads to security vulnerabilities. Serializing a closure means to serialize its local state, that is you have to make the closure's free variables visible for the surrounding scope:

const RetrieveArgs = Symbol();

const metaApply = f => x => {
  const r = f(x);

  if (typeof r === "function") {
    if (f[RetrieveArgs])
      r[RetrieveArgs] = Object.assign({}, f[RetrieveArgs], {x});
  
    else r[RetrieveArgs] = {x};
  }

  return r;
}

const add = m => n => m + n,
  f = metaApply(add) (10);

console.log(
  JSON.stringify(f[RetrieveArgs]) // {"x":10}
);

const map = f => xs => xs.map(f)
  g = metaApply(map) (n => n + 1);

console.log(
  JSON.stringify(g[RetrieveArgs]) // doesn't work with higher order functions
);

I use a Symbol in order that the new property doesn't interfere with other parts of your program.

As mentioned in the code you still cannot serialize higher order functions.




回答3:


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 !



来源:https://stackoverflow.com/questions/49743848/how-to-correctly-serialize-javascript-curried-arrow-functions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!