(blog-of ‘Alex’)

A personal blog about software development and other things. All opinions expressed here are my own, unless explicitly stated.

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")
}