Last active
August 29, 2015 14:16
-
-
Save willurd/8ae804f51aa6f3ea1708 to your computer and use it in GitHub Desktop.
Parametric partial function creation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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]') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice! ⭐