// I had this idea of implementing Scala-ish automatic partial function generation // based on placeholders, and Clojure-ish positional and rest argument replacement // in anonymous functions created with the shorthand syntax `#()`. // In Scala, if you have an expression such as `doSomething(_ + 1)`, Scala will // turn that into a function of one argument that evaluates to `arg + 1`. In // Clojure, if you use `#()` to create an anonymous function, you can reference // arguments with `%num` (e.g. `%1`, `%2`, etc). `%` evaluates to the first // argument and `%&` evaluates to all arguments. // This code connects those two ideas with a function, `fn`, which wraps other // functions to turn them into functions that can accept placeholders // (such as `_1`, `_2`, `_$`, etc). Functions wrapped with `fn` that are // given placeholders will be turned automatically into new, partially // applied functions that will continue to apply new arguments until their // original function signatures are fully satisfied. // jsbin for the tinkerers: http://jsbin.com/veqoso/1/edit?js,console // ====================================================================== // Placeholder definitions // ====================================================================== class Placeholder { canAlwaysEval() { return false; } canEval() { return false; } eval(args) { return null; } } // Replaced with the argument at it's saved index. class IndexPlaceholder extends Placeholder { constructor(index) { this.index = index; } toString() { return `[IndexPlaceholder(${this.index})]`; } canAlwaysEval() { return false; } canEval(signature, args, index) { return this.index <= (args.length - 1) && !(this.eval(signature, args, index) instanceof Placeholder); } eval(signature, args, index) { return args[this.index]; } } // Replaced with the argument at its own index. class AutoPlaceholder extends Placeholder { toString() { return `[AutoPlaceholder]`; } canAlwaysEval() { return false; } canEval(signature, args, index) { return index <= (args.length - 1) && !(this.eval(signature, args, index) instanceof Placeholder); } eval(signature, args, index) { return args[index]; } } // Replaced with the extra arguments. class RestPlaceholder extends Placeholder { toString() { return `[RestPlaceholder]`; } canAlwaysEval() { return true; } canEval(signature, args, index) { return true; } eval(signature, args, index) { return args.slice(signature.length); } } // Replaced with the full list of all arguments. class AllPlaceholder extends Placeholder { toString() { return `[AllPlaceholder]`; } canAlwaysEval() { return true; } canEval(signature, args, index) { return true; } eval(signature, args, index) { return args; } } // Saved as a function so others can create more placeholders // if they need them, such as `var _42 = placeholder(42);`. var placeholder = index => new IndexPlaceholder(index); // Default placeholders. This should be enough for most use cases. var _ = new AutoPlaceholder(); var _0 = placeholder(0); var _1 = placeholder(1); var _2 = placeholder(2); var _3 = placeholder(3); var _4 = placeholder(4); var _5 = placeholder(5); var _6 = placeholder(6); var _7 = placeholder(7); var _8 = placeholder(8); var _9 = placeholder(9); var _$ = new RestPlaceholder(); // leftover arguments var _$$ = new AllPlaceholder(); // all arguments // ====================================================================== // Magical function wrapper // ====================================================================== // Returns true if the given signature is fullfilled by the given arguments. var fnFullfilled = (signature, args) => { return signature.every((arg, index) => { return !(arg instanceof Placeholder) || arg.canEval(signature, args, index); }); }; // Evaluates the given signature with the given arguments. var fnEval = (signature, args) => { args = args.filter(arg => !(arg instanceof Placeholder)); return signature.map((arg, index) => { if (arg instanceof Placeholder) { return arg.eval(signature, args, index); } else { return arg; } }); }; // Turns any function into a partial function automatically if it is // called with any partial placeholders (e.g. _0, _5, _, _$, etc). var fn = function(fun) { var first = true; // The signature is the set of arguments the function was originally called with. // If any of those arguments are placeholders, the signature is saved and a // new function is returned. return function(...signature) { // This is the recursive function. This basically implements currying, // except instead of checking arity it's checking completeness (as // defined by the ability to evaluate all placeholders). var inner = args => { var dontApply = first && args.length && args.every(arg => arg instanceof Placeholder && arg.canAlwaysEval()); if (dontApply || !fnFullfilled(signature, args)) { first = false; // There are more placeholders to satisfy. Keep going. return (...newArgs) => { var replaced = args.map(arg => { // Replace placeholders with arguments positionally as we receive them. // This builds up a list of arguments in order, which is separate from // the original signature which may request arguments out of order. if (newArgs.length && arg instanceof Placeholder) { return newArgs.shift(); } else { return arg; } }); // Add what's left of newArgs. return inner(replaced.concat(newArgs)); }; } else { // All placeholders have been satisfied. Evaluate them, then execute the // original function. return fun(...fnEval(signature, args)); } }; return inner(signature.slice()); }; }; // ====================================================================== // Tests // ====================================================================== var sub = fn((a, b) => a - b); // This is a partial function of two arguments because the first argument // is an index placeholder pointing at index 0, and the second argument is a // non-placeholder value. Once `subBy1` is called with a single, non-placeholder // value, `_0` will be replaced and `sub` (above) will be fully applied. var subBy1 = sub(_0, 1); console.log(subBy1(3) + ': should be 2'); console.log(subBy1(5) + ': should be 4'); // This is also a partial function of two arguments, except this time the // first argument is a non-placeholder value and the second argument is // an auto placeholder. Auto placeholders say "replace me with whatever // argument is at this index". In this case, that's index 1. Once `subFrom1` // is called with a single, non-placeholder value, `_` will be replaced // and `sub` will be fully applied. var subFrom1 = sub(1, _); console.log(subFrom1(3) + ': should be -2'); // The same index placeholder can be used for multiple arguments. `mult` // takes two arguments, but `square` only takes one. Once it is called // with a single argument, both `_0` instances get replaced by that // argument and `mult` is fully applied. var mult = fn((a, b) => a * b); var square = mult(_0, _0); console.log(square(3) + ': should be 9'); // Partially applied functions will continue to return new functions // until they receive all arguments. This is currying. var unnecessary = mult(_0, _1); console.log(unnecessary(4)(5) + ': should be 20'); // Index placeholders refer to the index of the arguments as given // to the partially applied function (`rest`, in this case). `_$` // will replace with the extra arguments given to `rest`. Extra is // defined by any number of arguments given after the number of // arguments specified in `rest`'s signature (`(_1, _$, _0)` in // this case). var test = fn((a, b, c) => { return { a, b, c }; }); var rest = test(_1, _$, _0); console.log(JSON.stringify(rest(1, 2, 3, 4, 5)) + ': should be { a: 2, b: [4, 5], c: 1 }'); // All of these placeholders will be replaced by the same argument. // `_$$` actually gets replaced with all arguments passed to the // partial, but in this case it's a list of a single item. `_0` // gets replaced with the first argument. `_` gets replaced with // the argument that shares its index in the function signature. // Since `_` shows up at index 0 in the function signature, it // will get replaced with the argument at index 0. var allTheSame = test(_, _0, _$$); console.log(JSON.stringify(allTheSame(5)) + ': should be { a: 5, b: 5, c: [5] }'); // You can ignore arguments completed by simply not adding // placeholders for them. This function requires six arguments // before being fully applied. It tosses the first three // and passes on the last three. var test = fn((a, b, c) => { return { a, b, c }; }); var skipAFew = test(_3, _4, _5); console.log(JSON.stringify(skipAFew(10, 9, 8, 7, 6, 5)) + ': should be { a: 7, b: 6, c: 5 }'); // This function takes no indexed arguments, and as such will be // fully applied the first time it is called, regardless of how // many arguments it is give. This particular function simply // collects all of its arguments and gives them back, showing // you the full list and the list of extra arguments as well. var restAndAll = fn((rest, all) => { return { rest, all }; }); var restAndAllPartial = restAndAll(_$, _$$); console.log(JSON.stringify(restAndAllPartial()) + ': should be { rest: [], all: [] }'); console.log(JSON.stringify(restAndAllPartial(1, 2, 3)) + ': should be { rest: [3], all: [1, 2, 3]}'); console.log(JSON.stringify(restAndAllPartial(1, 2, 3, 4)) + ': should be { rest: [3, 4], all: [1, 2, 3, 4]}'); // ====================================================================== // More elaborate example // ====================================================================== var isFunction = val => { return typeof val === 'function'; }; var get = fn((obj, prop) => { return obj[prop]; }); var doto = fn((value, ...funs) => { return reduce(funs, (acc, fun) => { return fun(acc); }, value); }); var pairs = obj => { var arr = []; for (var key in obj) arr.push([ key, obj[key] ]); return arr; }; var map = fn((arr, fun) => { return arr.map(fun); }); var filter = fn((arr, fun) => { return arr.filter(fun); }); var reduce = fn((arr, fun, init) => { return arr.reduce(fun, init); }); // Very contrived example to show off the terseness of automatic // partial function creation. This is in no way an example of good // code, nor the best way to implement this function. var methods = doto(_, pairs, filter(_, doto(_, get(_, 1), isFunction)), map(_, get(_, 0))); var o = { a: 1, b: () => {}, c: 2, d: () => {} }; console.log(JSON.stringify(methods(o)) + ': should be: [b, d]')