git » jacl.git » commit 58216b9

add multimethod

author Alan Dipert
2019-10-16 14:46:36 UTC
committer Alan Dipert
2019-10-16 14:46:36 UTC
parent e4fd8e7a6f22e00e1b639a51ebf5ac0410c1e7d7

add multimethod

multimethod.js +194 -0

diff --git a/multimethod.js b/multimethod.js
new file mode 100644
index 0000000..77f468e
--- /dev/null
+++ b/multimethod.js
@@ -0,0 +1,194 @@
+// multimethod: Creates functions — "multimethods" — that are polymorphic on one
+// or more of their arguments.
+//
+// Multimethods can take any number of arguments. Arguments are passed to an
+// applicable function or "method", returning its result. By default, if no
+// method was applicable, an exception is thrown.
+//
+// Methods are searched in the order that they were added, and the first
+// applicable method found is the one used.
+//
+// A method is applicable when the "dispatch value" associated with it
+// corresponds to the value returned by the dispatch function. The dispatch
+// function defaults to the value of the first argument passed to the
+// multimethod.
+//
+// The correspondence between the value returned by the dispatch function and
+// any method's dispatch value is determined by the test function, which is
+// user-definable and defaults to `equal` or deep equality.
+//
+// # Chainable Functions
+//
+// The function returned by `multimethod()` exposes functions as properties.
+// These functions generally return the multimethod, and so can be chained.
+//
+// - dispatch([function newDispatch]): Sets the dispatch function. The dispatch
+//   function can take any number of arguments, but must return a dispatch
+//   value. The default dispatch function returns the first argument passed to
+//   the multimethod.
+//
+// - test([function newTest]): Sets the test function. The test function takes
+//   two arguments: the dispatch value produced by the dispatch function, and
+//   the dispatch value associated with some method. It must return a boolean
+//   indicating whether or not to select the method. The default test function
+//   is `equal`.
+//
+// - when(object dispatchVal, function method): Adds a new dispatch value/method
+//   combination.
+//
+// - whenAny(array<object> dispatchVals, function method): Like `when`, but
+//   associates the method with every dispatch value in the `dispatchVals`
+//   array.
+//
+// - else(function newDefaultMethod): Sets the default function. This function
+//   is invoked when no methods apply. If left unset, the multimethod will throw
+//   an exception when no methods are applicable.
+//
+// - clone(): Returns a new, functionally-equivalent multimethod. This is a way
+//   to extend an existing multimethod in a local context — such as inside a
+//   function — without modifying the original. NOTE: The array of methods is
+//   copied, but the dispatch values themselves are not.
+//
+// # Self-reference
+//
+// The multimethod function can be obtained inside its method bodies without
+// referring to it by name.
+//
+// This makes it possible for one method to call another, or to pass the
+// multimethod to other functions as a callback from within methods.
+//
+// The mechanism is: the multimethod itself is bound as `this` to methods when
+// they are called. Since arrow functions cannot be bound to objects, **self-reference
+// is only possible within methods created using the `function` keyword**.
+//
+// # Tail recursion
+//
+// A method can call itself in a way that will not overflow the stack by using
+// `this.recur`.
+//
+// `this.recur` is a function available in methods created using `function`.
+// When the return value of a call to `this.recur` is returned by a method, the
+// arguments that were supplied to `this.recur` are used to call the
+// multimethod.
+//
+// # Examples
+//
+// Handling events:
+//
+//    var handle = multimethod()
+//     .dispatch(e => [e.target.tagName.toLowerCase(), e.type])
+//     .when(["h1", "click"], e => "you clicked on an h1")
+//     .when(["p", "mouseover"], e => "you moused over a p"})
+//     .else(e => {
+//       let tag = e.target.tagName.toLowerCase();
+//       return `you did ${e.type} to an ${tag}`;
+//     });
+//
+//    $(document).on("click mouseover mouseup mousedown", e => console.log(handle(e)))
+//
+// Self-calls:
+//
+//    var demoSelfCall = multimethod()
+//     .when(0, function(n) {
+//       this(1);
+//     })
+//     .when(1, function(n) {
+//       doSomething(this);
+//     })
+//     .when(2, _ => console.log("tada"));
+//
+// Using (abusing?) the test function:
+//
+//    var fizzBuzz = multimethod()
+//     .test((x, divs) => divs.map(d => x % d === 0).every(Boolean))
+//     .when([3, 5], x => "FizzBuzz")
+//     .when([3], x => "Fizz")
+//     .when([5], x => "Buzz")
+//     .else(x => x);
+//
+//    for(let i = 0; i <= 100; i++) console.log(fizzBuzz(i));
+//
+// Getting carried away with tail recursion:
+//
+//    var factorial = multimethod()
+//     .when(0, () => 1)
+//     .when(1, (_, prod = 1) => prod)
+//     .else(function(n, prod = 1) {
+//       return this.recur(n-1, n*prod);
+//     });
+//
+//    var fibonacci = multimethod()
+//     .when(0, (_, a = 0) => a)
+//     .else(function(n, a = 0, b = 1) {
+//       return this.recur(n-1, b, a+b);
+//     });
+function multimethod(dispatch = (firstArg) => firstArg,
+                     test = equal,
+                     defaultMethod = null,
+                     methods = []) {
+
+  var trampolining = false;
+
+  function Sentinel (args) { this.args = args; }
+
+  function trampoline(f) {
+    return (...args) => {
+      trampolining = true;
+      var ret = f.apply(invoke, args);
+      while (ret instanceof Sentinel)
+        ret = f.apply(invoke, ret.args);
+      trampolining = false;
+      return ret;
+    };
+  }
+
+  let invoke = trampoline((...args) => {
+    var dispatchVal = dispatch.apply(null, args);
+    for (let i = 0; i < methods.length; i++) {
+      let [methodVal, methodFn] = methods[i];
+      if (test(dispatchVal, methodVal)) {
+        return methodFn.apply(invoke, args);
+      }
+    }
+    if (defaultMethod) {
+      return defaultMethod.apply(invoke, args);
+    } else {
+      throw new Error(`No method for dispatch value ${dispatchVal}`);
+    }
+  });
+
+  invoke.recur = (...args) => {
+    if (!trampolining) throw new Error("recur can only be called inside a method");
+    return new Sentinel(args);
+  };
+
+  invoke.dispatch = (newDispatch) => {
+    dispatch = newDispatch;
+    return invoke;
+  };
+
+  invoke.test = (newTest) => {
+    test = newTest;
+    return invoke;
+  };
+
+  invoke.when = (dispatchVal, methodFn) => {
+    methods = methods.concat([[dispatchVal, methodFn]]);
+    return invoke;
+  };
+
+  invoke.whenAny = (dispatchVals, methodFn) => {
+    return dispatchVals.reduce((self, val) => invoke.when(val, methodFn), invoke);
+  };
+
+  invoke.else = (newDefaultMethod = null) => {
+    defaultMethod = newDefaultMethod;
+    return invoke;
+  };
+
+  invoke.clone = () => {
+    return multimethod(dispatch, test, defaultMethod, methods.slice());
+  };
+
+  return invoke;
+}