npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@banzai-inc/multimethod

v0.1.0

Published

Multimethods for JavaScript

Downloads

3

Readme

What is multimethod.js?

Multimethods are a functional programming control structure that allow you to dynamically build-up and manipulate the dispatching behavior of a polymorphic function. Inspired by clojure's multimethods, multimethod.js provides a functional alternative to classical, prototype based polymorphism. The multimethod.js library is 1kb minified, MIT licensed, and available on GitHub.

Installation

Install with npm for use in node.js based projects.

npm install multimethod
node
> var multimethod = require('multimethod');

For in-browser use you will need to grab underscore.js and multimethod.js:

  • underscore.js
    • Development: http://documentcloud.github.com/underscore/underscore.js
    • Minified: http://documentcloud.github.com/underscore/underscore-min.js
  • multimethod.js
    • Development: https://raw.github.com/KrisJordan/multimethod-js/master/multimethod.js
    • Minified: https://raw.github.com/KrisJordan/multimethod-js/master/multimethod-min.js

API

  • Constructor: multimethod( [fn | string] ): No arg constructor uses an identity function for dispatch. Single arg constructor is a shortcut for calling dispatch with the same argument.
  • dispatch(fn | string): Sets the multimethod's dispatch function. String values are transformed into a pluck function which projects a single property value from the first argurment.
  • when(match, fn | value): Add a method to be invoked when the dispatch return value matches 'match'. If a non-function value is provided it will be returned directly. Calling when with the same match value twice will override the previously registered method.
  • remove(match): Remove a method by it's match value.
  • default(fn | value): Catch-all case when no method match is found.

Motivating Examples

Let's use the node.js REPL to build a few multimethods and see what they are capable of doing. In this first example we'll create a mulimethod that calculates the area of shapes instantiated with object literals.

> var multimethod = require('multimethod');
> var area = multimethod()
                .dispatch(function(o) {
                    return o.shape;
                })
                .when("square", function(o) {
                    return Math.pow(o.side, 2);
                });
> var aSquare = { "shape":"square", "side": 2 };
> area( aSquare );
4

> var aCircle = { "shape":"circle", "radius": 5 };
> area( aCircle );
undefined

> area.default(function(o) { 
    throw "Unknown shape: " + o.shape;
  });
> area( aCircle );
Unknown Shape: circle

> area.when("circle", function(o) {
    return Math.PI * Math.pow(o.radius, 2);
  });
> area( aCircle );
78.53981633974483
> area( aSquare );
4

> area.remove("circle");
> area( aCircle );
Unknown Shape: circle

Notice how dispatch returns the value we'll match to a "method" registered with when. You can introduce, overwrite, and remove new methods dynamically at runtime. Fallback behavior can be established with a default function called when no methods match the dispatched value.

A recursive Fibonacci function can be expressed naturally with a multimethod.

> var fib = multimethod()
                .when( 0, 0 )
                .when( 1, 1 )
                .default( function(n) {
                    return fib(n-1) + fib(n-2);
                });
> fib(20);
6765

Notice, there is no dispatch specified. By default a multimethod will use the first argument it is invoked with to match the correct method to dispatch to.

> var hitPoints = multimethod()
                    .dispatch(function(player){ return player.powerUp; })
                    .when( {"type":"star"} , Infinity)
                    .default(5);

> var starPower = { "type":"star" },
>     mario = { "powerUp": starPower };
> hitPoints(mario);
Infinity

> mario.powerUp = null;
> hitPoints(mario);
5

> var godModeCheat = function() { return starPower; };
> hitPoints.dispatch(godModeCheat);
> mario.powerUp;
null
> hitPoints(mario);
Infinity

In this last example notice how we are matching against an object. Matching is done using deep equality so objects and arrays are valid method matching criteria. Also notice how we can completely override our dispatch function to change the logic with which a multimethod evaluates its arguments for dispatch, or, in this case, ignores them!

Multimethod Dispatch Algorithm Overview

  1. User calls multimethod with argument anArgument.
  2. Multimethod calls its dispatch function with anArgument. The returned value is stored in dispatchValue.
  3. Multimethod iterates through each 'method' registered with when and performs an equality test on the dispatchValue and each method's match value. If a match is found, set matchFunction to the method's function and go to step 5.
  4. If no method match found, set matchFunction to the multimethod's default function.
  5. Multimethod calls matchFunction with anArgument. The returned value is returned to the user who called the multimethod.

Detailed Walkthrough

The Basics

A multimethod is instantiated with the multimethod function.

var stopLightColor = multimethod();

A multimethod has methods. A method is has two parts, its match value and its implementation function. Methods are added using when.

stopLightColor.when("go",    function() { return "green"; })
              .when("stop",  function() { return "red"; });

You can call a multimethod just like any other function. It will dispatch based on the argument(s) passed in, invoke the matched method, and return the results of the method call.

console.log( stopLightColor("go") ); // "green"

When no method matches control can fallback to a default method.

stopLightColor.default( function() { return "unknown"; } );
console.log( stopLightColor("yield") ); // prints "unknown"

A multimethod can handle new cases dynamically at run time.

stopLightColor.when("yield", function() { return "yellow"; });

There is a shorter way for a method to return a plain value. Rather than passing an implementation function to when, pass the value.

stopLightColor.when("yield", "yellow");
console.log( stopLightColor("yield") ); // prints "yellow"

A method can be removed dynamically at run time, too.

stopLightColor.remove("go");
console.log( stopLightColor("go") ); // prints "unknown"

Dispatch Function

Each multimethod call first invokes a dispatch function whose return value is used to match the correct method to call. The dispatch function is passed the arguments the multimethod is invoked with and returns a value to match against.

The default dispatch function is an identity function. The basic stopLightColor examples could have been created with an explicit dispatch function.

var stopLightColor = multimethod()
      .dispatch(function(state){
         return state;
      })
      .when('go', 'green');
console.log( stopLightColor('go') ); // green

The power of the multimethod paradigm is the ability to dispatch with a user-defined function. This gives a multimethod its "polymorphic" powers. Unlike classical, object-oriented polymorphism where the compiler dispatches based on the type hierarchy, a multimethod can dispatch on any criteria.

var contacts = [
  {"name":"Jack", "service":"Twitter","handle": "@jack"},
  {"name":"Diane","service":"Email",  "address":"[email protected]"},
  {"name":"John", "service":"Phone",  "number": "919-919-9191"}
];

var sendMessage = multimethod()
     .dispatch(function(contact, msg) {
       return contact.service;
     })
     .when("Twitter", function(contact, msg) {
       console.log("Tweet @"+contact.handle+":"+msg);
     })
     .when("Email", function(contact, msg) {
       console.log("Emailing "+contact.address+":"+msg);
     })
     .default(function(contact, msg) {
       console.log("Could not message " + contact.name);
     });

// Blast a message
contacts.forEach( function(contact) {
  sendMessage(contact, "Hello, world."); 
});

Plucking a single property from an object is so commonly used as a dispatch function, like in the example above, there is a shortcut for this pattern. The following dispatch call is equivalent to above.

sendMessage.dispatch( 'service' );

A multimethod's dispatch is usually specified when constructed.

var sendMessage = multimethod('service');

Just like methods can be added and removed from a multimethod at run time, the dispatch function can also be redefined at run time. Ponder the implications of that for a minute. It is really powerful and really dangerous. Don't shoot your eye out.

Deep Equality Matching

A method's match value is compared to dispatch's return value using the underscore.js isEqual function. Deep equality method matching enables concise expressivity. Contrast this with a traditional switch statement that is limited by JavaScript's === equality behavior.

var greatPairs = multimethod()
      .when( ["Salt", "Pepper"], "Shakers" )
      .when( [{"name":"Bonnie"}, {"name":"Clyde"}], "Robbers" );
console.log( greatPairs( ["Salt", "Pepper"] ) ); // Shakers

Related Work

  • Clojure's multimethod - http://clojure.org/multimethods
  • Pascal Coste Filtered Dispatch in Common Lisp - http://www.p-cos.net/documents/filtered-dispatch.pdf

How-to Contribute

  • Submit bugs and feature requests on GitHub Issues page.
  • Fork the repository and submit pull requests. Pull requests that update the test suite for coverage on changes will be brought in quickly.