This is part two on implementing Rust style pattern matching in JS. Click here for part one.

So far:

  • We’ve defined an interface to describe a Matchable object.
  • We’ve defined a match function that hijacks a Matchable instance method.
  • We’ve defined a sum_type object factory function which can produce a variety of sum types.

…and that’s about all.

In this article I’m going to take a hard deviation from pattern matching and talk about some Functional Programming concepts. We’ll discuss what this whole matchable sum types thing is about under the covers and how to add some very functional…functions to our sum types.

At the end of part one, I mentioned our original implementation has flaws. The primary shortcoming I mentioned was that the object we’ve constructed using the `sum_type` factory function has no prototype and is therefore not extensible. You could never change the values it has upon construction by defining on its prototype because that prototype is `undefined`. But there are actually a few other flaws that I can think of. Like the fact that two objects from different sum type sets, but with the same name will match (in)correctly. And you can’t match objects you didn’t make through the sum_type function, either.

I aim to solve all those problems in this series, but let’s start with the prototype problem. Let’s look at the sum_type function:

const sum_type = constructors => {
const T = {}
Object.entries(constructors).forEach(([tag, ctor]) => {

const result = function(...args) {
const pvt = ctor(...args) || {}
Matchable.call(this, tag, pvt)
}

T[tag] = function(...args){ return new result(...args) }

})
return T
}

T[tag] is a function that returns a new result but the function itself is not the actual result constructor. It’s just an unnamed function that we use to obscure the use of new. So let’s try to add a prototype to that function and make it point to the same prototype used by result.

const sum_type = constructors => {
const T = {}
Object.entries(constructors).forEach(([tag, ctor]) => {

const result = function(...args) {
const pvt = ctor(...args) || {}
Matchable.call(this, tag, pvt)
}
T[tag] = function(...args){
//these two functions now refer to the same prototype
result.prototype = T[tag].prototype
return new result(...args)
}
})
return T
}

That was easy. Now let’s see how we can use it. Add these lines to the top of the file:

const prop    = Object.getOwnPropertyDescriptor,
defprop = Object.defineProperty,
entries = Object.entries,
symbols = Object.getOwnPropertySymbols

Now let’s write a little homebrewed extend function, the likes of which could be found in functional libraries like ramda, underscore, and others.

const extend = (t, ...exts) => {
exts.forEach(ext => {
entries(ext).forEach(([k,v]) => {
k === 'prototype' ? t[k] = v
: /*else*/ defprop(t, k, prop(ext, k))
})
symbols(ext).forEach(s => {
defprop(t, s, prop(ext, s))
})
})
}

What this does is take a target object t and extends it by adding all of the properties found on each of the source objects ...exts. We do so by looping through all of the (enumerable) entries on the object, special-casing prototype so it will be set as expected. Then it loops through the symbols on the source objects, if there are any, and adds them to the target as well. It’s actually pretty handy for mixing functionality into objects.

Now to bring some of this together — my goal has been to be able to extend the sum types we make with our sum_type factory function with functions that operate on the backing values that we’ve been storing inside our Matchable constructor via the pvt parameter. We are encapsulating data in the function that isn’t accessible from outside. That is really what we want to do if we are to protect that data from unwanted modifications. But, as the author of this mini-library, we need a special way to look at that data. That’s because we are going to use it from outside of the function context where pvt is valid.

So let’s define another Symbol to key our private data in a way that is not documented to the end user of our types:

//... 
const TAG = Symbol('tag-sym'),
SECRET = Symbol('secret-sym')
//...

And then we’ll use that SECRET Symbol in the Matchable constructor.

function Matchable(tag, pvt) {
this[TAG] = tag
this[SECRET] = pvt //now we can see pvt from the outside SECRETly
this.match = function(pattern) {
return (// v~~~this is the private data, accessible in this scope.
tag in pattern ? pattern[tag](pvt)
: '_' in pattern ? pattern['_']()
: /*else*/ err('no match found for '+tag)
)
}
}

OK, now we’re getting somewhere. I can access the pvt parameter using our new Symbol. Let’s see if we can add a function to an object we created with sum_type. First we’ll define a type:

const { A, B } = sum_type({
A(value) { return {value} },
B(){}
})

This is basically the equivalent of the Option type in rust. A(value) is just like Some(value), and Bis just like None. I used to call this type Maybe. Now we have a simple wrapper type that can house something or nothing in a very Schroedinger-esque scenario. But as it stands it’s just a box and it doesn’t help us do anything with the content, except defend against null/undefined and be useful in our match function. Let’s make it a Functor — that’s a box that can map its contents, much like Array, except in this scenario we’re not working with a bunch of possible values, just ‘a value’ or ‘not a value’. Array is in fact a functor too because it can map but that’s a discussion for another time, and has been better defined by many other more prominent functional programmers than I. Here’s that map function’s definition:

extend(A.prototype, { 
map(f) { return A(f(this[SECRET].value)) }
})

So to map is to apply a function f to the contents of an instance of A while not taking it out of it’s box. In truth though, to be a functor you should be able to take the contents out of the box, too. We could call this function a bunch of things: join, flatten, unwrap and probably other flavors as well. But they just mean ‘take my contents out as a plain value’. Let’s add toString,too, just for native JS integration.

extend(A.prototype, {
map(f) { return A(f(this[SECRET].value)) },
unwrap() { return this[SECRET].value },
toString() { return 'A('+this[SECRET].value+')' }
})

But now B is missing functionality that should be implemented for both variants. Let’s fix that:

extend(B.prototype, {
map(f) { return this },
unwrap() { return this },
toString() { return 'B' }
})

If we have an empty box, as we’ve represented by B, we don’t want it to do anything at all when we map it or unwrap it.

I think this is a good place to stop this article. We’ve succeeded in making our sum_types extensible, and we learned a tiny bit about Functors along the way. If your palette has been whetted by this taste of Functors and functional programming, have a look at Professor Frisbee’s Mostly Adequate Guide to Functional Programming. It’s probably the best JS guide to FP.

Check out the Pt 2 pen for some examples of how to use it, as well: