Skip to content

Instantly share code, notes, and snippets.

@sliminality
Last active December 20, 2018 21:27
Show Gist options
  • Save sliminality/f6da1f9104c8b4a04ebaa4e154f6f264 to your computer and use it in GitHub Desktop.
Save sliminality/f6da1f9104c8b4a04ebaa4e154f6f264 to your computer and use it in GitHub Desktop.
This gist is where I will post my weird and inefficient JavaScript metaprogramming experiments

Dot composition

f.g.h(x) === f(g(h(x)))

Example

const add1 = x => x + 1;
const double = x => x * 2;
const square = x => x ** 2;
const _ = makeComposable({ add1, double, square });

console.assert(
  _.double.add1(1) === double(add1(1))
);
console.assert(
  _.double.add1.add1.square(100) === double(add1(add1(square(100))))
);
console.assert(
  _.add1.square.add1.double(3) === add1(square(add1(double(3))))
);

Idea

  1. Trap property lookups, and redirect all method lookups to a Map of the instrumented functions. This enables chained lookup, e.g. double.square.add1.
  2. Trap function applications, and pass the result to another pre-bound function. This enables function composition, e.g. double.square(5) is really double(square(5)).

Caveats

Notational abuse is confusing and this should probably never be used in practice. Additionally, here are some other glaring problems with this implementation:

  • Only works with unary functions (i.e. functions of a single argument) right now
  • Does absolutely nothing to handle naming collisions with the prototype chain (e.g. apply)
  • One zillion other edge cases I haven't thought about
// Takes an object containing unary functions, and makes those functions composable using the dot `.` operator.
function makeComposable(methods) {
const _methods = new Map();
const remapCalls = next => ({
// Remap invocations of the proxied object to call `next` after completion.
apply(target, thisArg, argumentList) {
return next(Reflect.apply(target, thisArg, argumentList));
}
});
const remapGets = {
get(target, key, receiver) {
if (_methods.has(key)) {
// If both `target` and `key` are instrumented methods, return a Proxied `key` method
// that traps applications and also invokes `target`.
// This composes: if the `key` Proxy is subsequently chained to another method,
// invoking that method will in turn trigger `key`'s trap to call `target`.
return typeof target === 'function'
? new Proxy(_methods.get(key), remapCalls(receiver))
: _methods.get(key);
}
return Reflect.get(target, key);
}
};
// Wrap each method in a proxy to trap subsequent method lookups.
for (const name of Object.keys(methods)) {
const method = methods[name];
_methods.set(name, new Proxy(method, remapGets));
}
return new Proxy(methods, remapGets);
}
const add1 = x => x + 1;
const double = x => x * 2;
const square = x => x ** 2;
const _ = makeComposable({ add1, double, square });
console.assert(
_.double.add1(1) === double(add1(1))
);
console.assert(
_.double.add1.add1.square(100) === double(add1(add1(square(100))))
);
console.assert(
_.add1.square.add1.double(3) === add1(square(add1(double(3))))
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment