Pattern-matching in a functional way
Several days ago I needed an easy to use and easy to extend facility that performs pattern matching.
I ended up with the following interface:
// Crux of this library, combine function that takes a pattern and function which
// gets invoked when corresponding pattern matches certain given object.
// The result of combine function is another function, that can be used on objects.
//
// To prevent conditions when no pattern matches the given object,
// it might be initialized as follows:
// matcher = function() { throw new Error("no applicable pattern found") }
var matcher
matcher = combine(PATTERN1, CALLBACK1(OBJ, .. OPTIONAL_ARGS){...}, matcher)
matcher = combine(PATTERN2, CALLBACK2(OBJ, .. OPTIONAL_ARGS){...}, matcher)
// the result of combine method shall be immediately callable to perform pattern matching
matcher = combine(PATTERN3, CALLBACK3(OBJ, .. OPTIONAL_ARGS){...}, matcher)
// ...
// Apply pattern matching function on certain object:
matcher(OBJ, ... OPTIONAL_ARGS)
And here is how one can use it:
var matcher = function(val, arg) {
print("matcher fallback: val = " + val + ", arg = " + arg)
}
matcher = pm.combine({type: "string"}, function(val, arg) {
print({expr: "matcher(stringVal, arg)", value: "val = " + val + ", arg = " + arg})
}, matcher)
matcher = pm.combine({instanceOf: Function}, function(val, arg) {
print({expr: "matcher(functionVal, arg)", value: "val = " + val + ", arg = " + arg})
}, matcher)
matcher = pm.combine({scheme: {key: "number", value: "any"}}, function(val, arg) {
print({expr: "matcher({key:number, value:any}, arg)", value: "val = (" + val.key + "," + val.value + "), arg = " + arg})
}, matcher)
matcher(5, "one")
matcher("str", "two")
matcher(new Function("return 1"), "three")
matcher({key: 12, value: 34}, "four")
matcher({key: "some", value: "unk"}, "five")
Here is an implementation:
// namespace
var pm = {}
/**
* Matcher functions constructors are used in pm.combine method.
* Each key in this object corresponds to the certain pattern member.
*/
pm._matcherConstructors = {
instanceOf: function (matcher, instanceTarget) {
return function (obj) {
if (obj instanceof instanceTarget) {
return matcher.apply(this, arguments)
}
return false
}
},
type: function (matcher, typeId) {
return function (obj) {
if (typeof(obj) === typeId) {
return matcher.apply(this, arguments)
}
return false
}
},
scheme: function (matcher, scheme) {
return function (obj) {
if (typeof(obj) !== "object") {
return false
}
for (var i in scheme) {
if (i in obj) {
var target = obj[i]
var source = scheme[i]
var sourceType = typeof(source)
if (sourceType === "string") {
if (source === "any" || source == typeof(target)) {
continue
}
return false
}
if (source !== target) {
return false
}
}
else {
return false
}
}
return matcher.apply(this, arguments)
}
}
}
/**
* Creates pattern matching function that accepts the pattern given.
* The latter combined patterns takes priority over the previously declared ones.
* @param pattern Pattern to match the target object.
* @param callback User-defined callback to accept target object as well as the accompanying arguments.
* @param prevMatcher Previous matcher function created by combine method or null or undefined.
* @returns Matcher function to be used as follows: matcher(objectToBeMatched, optionalArguments...).
*/
pm.combine = function(pattern, callback, prevMatcher) {
var matcher = function() {
callback.apply(this, arguments)
return true
}
// join visitor function according to the pattern given
for (var i in pattern) {
if (!(i in pm._matcherConstructors)) {
throw new Error("unexpected pattern tag: " + i)
}
matcher = pm._matcherConstructors[i](matcher, pattern[i])
}
// if prev matcher either undefined or null - create new function
if (prevMatcher == null) {
return matcher
}
else {
return function() {
if (matcher.apply(this, arguments)) {
return true
}
return prevMatcher.apply(this, arguments)
}
}
}
/**
* Helper function that initializes matcher for all the types of objects with
* the callback that throws an error.
*/
pm.unknownObjectMatcher = function() {
throw new Error("unknown object matched")
}