Skip to content

Instantly share code, notes, and snippets.

@willurd
Last active August 29, 2015 14:16
Show Gist options
  • Save willurd/8ae804f51aa6f3ea1708 to your computer and use it in GitHub Desktop.
Save willurd/8ae804f51aa6f3ea1708 to your computer and use it in GitHub Desktop.
Parametric partial function creation
// 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]')
@raganwald
Copy link

Nice! ⭐

@willurd
Copy link
Author

willurd commented Mar 9, 2015

@raganwald Thanks! 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment