Let's Implement Multiple Dispatch in JavaScript

Introduction

Multiple dispatch is the way for a function name to be associated with multiple function bodies based on certain properties of arguments.

For example, when adding two numbers, e.g. 1 + 2, the + symbol means mathematical addition, while if we “add” two strings, e.g. "Hello, " + "World!", the + symbol means string concatenation. Note how the symbol + remained the same but the operation changed based on if we were performing the operation on strings or numbers.

This is similar to (but not the same as) the concepts called “function/operator overloading” that are present in many programming languages like C++.

Our Goal

Our goal is to come up with a solution in JavaScript so that we could

Let’s think.

We’ll need to store all function definitions along with the data for determining which function is called.

Let’s store them as an array of functions. And with each function, let there be a “check” function associated with it. Our strategy will be to loop through the array and execute the function whose check function returns true.

//let's call this function that has multiple bodies a "generic function"
var GenericFunction = function () {
  //and let's call all the sub-functions "methods"
  this.methods = []
}

GenericFunction.prototype.defmethod = function (check, executor) {
  return this.methods.push({
    check: check,
    executor: executor //this will be the actual method body
  })
}

//take note not to confuse this method named "call" with the in-built JS functions' call property
GenericFunction.prototype.call = function (ctx, args) {//the ctx and args to be passed to the method
  for (var i = 0; i < this.methods.length; i++) {
    if (this.methods[i].check.apply(ctx, args)) {
      return this.methods[i].executor.apply(ctx, args)
    }
  }
}

//example usage

var add = new GenericFunction()

add.defmethod (
  function (a, b) {return typeof a === "number" && typeof b === "number" },
  function (a, b) {
    return a + b
  }
)

// to concat two arrays
add.defmethod (
  (a, b) => Array.isArray(a) && Array.isArray(b), //looks prettier with ES6 Arrow function notation
  function (a, b) {
    return [].concat(a).concat(b)
  }
)

console.log (
  add.call(this, [1, 2])
) //logs 3

console.log (
  add.call(this, [[1, 2, 3], [4]])
) //logs [1, 2, 3, 4]

Alright, we have something. It still doesn’t look very pretty, so the benefits aren’t apparent. Let’s make some cosmetic changes.


//we'll call this instead of constructing a new GenericFunction
var defgeneric = function () {
  var genericFunction = new GenericFunction()
  
  //this is the function we'll return so we can directly call the function instead of calling an object method
  var call = function (...methodCallArgs) {
    return genericFunction.call(this, methodCallArgs)
  }
  
  //adding methods will be the same. Since functions are objects, we can define additional properties on them
  call.defmethod = function (...args) {
    genericFunction.defmethod(...args)
    return call //for method call chaining
  }
  
  return call
}

//example usage
//add is a function this time, not an object. defgeneric call returns a function.
var add = defgeneric()

//but it has a .defmethod property defined on it
add.defmethod (
  (a, b) => typeof a === "number" && typeof b === "number",
  function (a, b) {
    return a + b
  }
)

add.defmethod (
  (a, b) => Array.isArray(a) && Array.isArray(b), 
  function (a, b) {
    return [].concat(a).concat(b)
  }
)

console.log (
  add(1, 2) //looks better!
)

console.log ( 
  add([1, 2, 3], [4])
) 

This is actually pretty much it! We could add some additional features though.

Also note how the check/predicate function needn’t be limited to checking types, but it can be any function! This is actually more powerful!

Additional Features

Now let’s add to our goals the following

  1. throw error when none of the methods match
  2. (optional) throw error when more than one of the methods match
  3. (optional) don’t throw error when more than one of the methods match, but execute the last matched method

First is self-explanatory, let’s discuss 2 and 3.

Let’s say your function you want to define is the following mathematical function

f(x) = x^2 if x < 3, x + 3 if x >= 3

Now if we calculate f(x) for x = 3 we’ll use the x + 3 definition only and the result will be 6. That is to say that for every possible input, only one of the function bodies will match and give the desired result.

Now we want to guard against logic errors in such scenarios, e.g. if the programmer writes the first check condition as (x) => x <= 3 and the second as (x) => x >= 3, the code we have will match the first function body and we’ll get a wrong result – 3 squared = 9.

So in such cases we can provide an option to declare that an error should be thrown in such a scenario.

Now the last proposition is the one I find the most interesting. It’s not too hard to implement, in the defintion of GenericFunction.prototype.call we’ll simply make the for loop loop backwards, i.e. for (var i = this.methods.length - 1; i >= 0; i--).

But implementation aside, let’s look at what it gives us.

With this, we can have the ability to override a previously defined method, e.g.

var f = defgeneric()

f.defmethod (
  (a) => typeof a === "number", 
  function (a) {
    //... more code
  }
)

//this will "override" the previous one, i.e. the previous one will never execute
f.defmethod (
  (a) => typeof a === "number",
  function (a) {
    //... more code
  }
)

Also, we get the ability to get the “more specific match”, e.g.

var area = defgeneric()

//will be called when a inherits from Quadrilateral
area.defmethod (
  (a) => a instanceof Quadrilateral, 
  function (a) {
    //complex code to calculate area of a quadrilateral
  }
)

//but the above will not be called when it inherits from a Rectangle even though all Rectangles inherit from Quadrilaterals for this example
area.defmethod (
  (a) => a instanceof Rectangle,
  function (a) {
    return a.height * a.width
  }
)

Which means that using the added goal #3, we can actually mimic how inheritance works and how we “override” methods of parent class in the subclasses.

And that’s the power of multiple dispatch.

I won’t actually walk you through adding these 3 goals. For the simple reason that it’s too boring. However, I have implemented these, you can check out the code here.

Closing Notes

Also the main code file should be easy to read if you’ve read the code in this article.

You can also install it using NPM via npm i peey/js-multiple-dispatch