├── README.md ├── lib └── unification.js ├── package.json └── test └── unification-test.js /README.md: -------------------------------------------------------------------------------- 1 | ## JUnify ― JavaScript Unification Library 2 | 3 | JUnify is a JavaScript library for performing [unification](http://en.wikipedia.org/wiki/Unification) on objects and arrays. It works on both the browser and Node.js. Unification is an algorithm to determine the substitutions needed to make two expressions match. If the expressions contain variables, these will need to be bound to values in order for the match to succeed. If two expressions are not identical or the variables can not be bound, the match fails. In the following example unification is used to extract values from a [JSON](http://www.json.org/) object using an pattern object: 4 | 5 | var point = { 6 | coords: [12, 10, 80], 7 | color: [255, 0, 0] 8 | }; 9 | 10 | var pattern = { 11 | coords: [variable('x'), variable('y'), _], 12 | color: variable('color') 13 | }; 14 | 15 | // a.x = 12, a.y = 10, a.color = [255, 0, 0] 16 | var a = unify(pattern, point); 17 | 18 | The syntax and use-case in this example is similar to [destructuring assignment in JavaScript 1.7](http://developer.mozilla.org/en/docs/New_in_JavaScript_1.7#Destructuring_assignment). Its use is however not limited to extracting fields. Unification can, for example, also be used to implement pattern matching or an expert system (Artificial Intelligence, Structures and Strategies for Complex Problem Solving, by George F. Luger. Addison Wesley, ISBN: 0-201-64866-0, page 68.) The following articles give some examples of the features that could be implemented using the JUnify library. 19 | 20 | * [Extracting values from JavaScript objects](../../writing/extracting-object-values.html) 21 | * [Pattern matching in JavaScript](../../writing/pattern-matching.html) 22 | * [Advanced pattern matching in JavaScript](../../writing/advanced-pattern-matching.html) 23 | 24 | ## Installation 25 | 26 | Use npm to install `junify`: 27 | 28 | > npm install junify 29 | 30 | You can then `require('junify')` and get access to the API explained below. 31 | 32 | ## API 33 | 34 | All code in the library is contained in the `junify` package. To keep the examples in this section simple, the package name is left out, but it should be present in any real code. The package exposes three public methods and one constant. The two most important methods are `unify` and `variable`. 35 | 36 |
37 |
unify(pattern1, pattern2)
38 |
Unifies pattern1 with pattern2. Returns `false` if the unification fails, otherwise it returns an object containing the variable bindings necessary for the unification.
39 | 40 |
variable(name, type)
41 |
Creates a new named variable with an optionally specified type. The returned variable can be used in the patterns when calling `unify`.
42 |
43 | 44 | Both the unify and variable methods were demonstrated in the introduction. Note however that the unification works both ways, that is, both patterns can contain variables: 45 | 46 | var a = { text: 'Hello', name: variable('name') }; 47 | var b = { text: variable('text'), name: 'World!' }; 48 | 49 | var c = unify(a, b); // c.text = 'Hello', c.name = 'World!' 50 | 51 | Variable names can however occur only once per pattern; they must be unique. 52 | 53 | JUnify can only perform unification on objects and arrays, not on atoms. The following types are considered atoms: `Boolean`, `Number`, `String`, `Function`, `NaN`, `Infinity`, `undefined` and `null`. Variables can also be typed, so they only match if the types are identical in both patterns. 54 | 55 | var a = new Date(); 56 | var b = new Boolean(true); 57 | var c = unify(variable('date', Date), a); // c.date = Mon Jun 23 2008 (…) 58 | var d = unify(variable('date', Date), b); // d = false 59 | 60 | The introduction also demonstrated the use of the wildcard constant `_`, which can be used to match an item (atom, array, or object) but does not create a binding. 61 | 62 |
63 |
_
64 |
Wildcard variable which matches any atom, array or object, but does not create any binding.
65 |
66 | 67 | The wildcard constant can also be used instead of an object property name, effectively matching any other object against it (but again, not creating a binding.) It is important that the wildcard symbol is *not* renamed (i.e. assigned another variable name,) as the library uses it internally to identify wildcard object property names. An example of both uses: 68 | 69 | var a = { text: 'Hello', name: 'World!' }; 70 | var b = { text: _, name: variable('name') }; 71 | var c = { text: _, var: _ }; 72 | var d = { text: _, _:_ }; 73 | 74 | var r = unify(a, a); // r = {} 75 | var r = unify(a, b); // r.name = 'World!' 76 | var r = unify(a, c); // r = false (no match) 77 | var r = unify(a, d); // r = {} (d can be matched against any object with 78 | // two properties, one of them being "text") 79 | 80 | The last method is `visit_pattern`, which is used to traverse a pattern using a visitor object with callbacks. This can be used to rewrite custom pattern syntax before passing it to the `unify` method. 81 | 82 |
83 |
visit_pattern(pattern, visitor)
84 |
Traverse the pattern using thevisitor. The visitor should be an object containing callback functions for variables, wildcards, functions, objects and atoms. None of these are required; if a callback function is not available the item under inspection is returned unmodified. All callback functions should return a value if implemented, which is then inserted at its original position in the pattern. The following callback functions are available: 85 | 86 |
87 |
variable(value)
88 |
Called when the pattern visitor encounters a variable. The variable is supplied as a parameter.
89 |
wildcard()
90 |
Called when the pattern visitor encounters a wildcard variable.
91 |
func(value)
92 |
Called when the pattern visitor encounters a function. The function is supplied as a parameter.
93 |
object(name, value)
94 |
Called when the pattern visitor encounters an object. The property name and value are supplied as parameters. The value parameter is visited *after* calling the object callback. The callback function should only return the value. It can not modify the key.
95 | 96 |
atom(value)
97 |
Called when the pattern visitor encounters an atom. The atom is supplied as a parameter.
98 |
99 |
100 | 101 | An example of using the `visit_pattern` method can be found in [the article on extracting values from JavaScript objects](../../writing/extracting-object-values.html), where it is used to implement a simplified syntax for extracting object properties. 102 | 103 | ## Example 104 | 105 | In this example we set up some variable names for convenience (you can also use the fully qualified names without any problems―I would actually recommend it.) Remember that the wildcard constant `_` must be an underscore. The methods can be renamed freely. We also create a function to display the results. If the unification succeeds this function will display all the variables and their bindings, or if the unification fails, display an error message. 106 | 107 | var _ = unification._; 108 | var $ = unification.variable; 109 | var unify = unification.unify; 110 | 111 | function display(value) { 112 | var name; 113 | 114 | if (value) { 115 | for (name in value) { 116 | if (value.hasOwnProperty(name)) { 117 | alert(name + " = " + value[name]); 118 | } 119 | } 120 | } 121 | else { 122 | alert("no match!"); 123 | } 124 | } 125 | 126 | It is then possible to use the library to either unify two patterns or extract values from objects. 127 | 128 | 129 | var a = [1, $('k'), 5, 7, 11]; 130 | var b = [1, 3, $('i'), 7, $('j')]; 131 | 132 | display(unify(a, b)); // i = 5, j = 11, k = 3 133 | 134 | var json = { 135 | article: { 136 | title: 'Pattern Matching in JavaScript', 137 | date: new Date(), 138 | author: 'Bram Stein' 139 | }, 140 | refid: '12480E' 141 | }; 142 | 143 | var pattern = { 144 | article: { 145 | title: $('title'), 146 | date: $('date', Date), 147 | author: $('author') 148 | }, 149 | _ : _ 150 | }; 151 | 152 | display(unify(pattern, json)); // title = 'Pattern Matching in JavaScript', 153 | // date = 'Mon Jun 23 2008 (…)', 154 | // author = 'Bram Stein' 155 | 156 | In the first example we unify two arrays, both containing variables. The returned object contains binding for all variables, from both arrays. The second example extracts the title, date and author properties from an article object if the date property has a type of `Date`. It returns an object with those properties if the match is succesfull. 157 | -------------------------------------------------------------------------------- /lib/unification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve JUnify - Unification JavaScript Library v1.0.1 3 | * 4 | * Licensed under the revised BSD License. 5 | * Copyright 2008-2012 Bram Stein 6 | * All rights reserved. 7 | */ 8 | /*jslint nomen: false */ 9 | /*global unify_aux */ 10 | var unification = (function () { 11 | var _ = 0xBAADF00D; 12 | 13 | var is_array = ( 14 | typeof Array.isArray !== 'undefined' ? 15 | Array.isArray : function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } 16 | ); 17 | 18 | function is_object(value) { 19 | return value && 20 | typeof value === 'object' && 21 | !is_array(value); 22 | } 23 | 24 | function is_function(value) { 25 | return value && 26 | typeof value === 'function'; 27 | } 28 | 29 | function is_boolean(value) { 30 | return value !== null && 31 | typeof value === 'boolean'; 32 | } 33 | 34 | function is_atom(value) { 35 | return typeof value !== 'object' || 36 | value === null || 37 | is_boolean(value); 38 | } 39 | 40 | function is_variable(value) { 41 | return value && 42 | typeof value === 'object' && 43 | typeof value.is_variable === 'function' && 44 | typeof value.get_name === 'function' && 45 | value.is_variable(); 46 | } 47 | 48 | function is_wildcard(value) { 49 | return value && value === _; 50 | } 51 | 52 | function variable(value, type) { 53 | return { 54 | is_variable : function () { 55 | return true; 56 | }, 57 | get_name : function () { 58 | return value; 59 | }, 60 | get_type : function () { 61 | return type || false; 62 | } 63 | }; 64 | } 65 | 66 | function occurs(variable, pattern) { 67 | var i, 68 | key; 69 | 70 | if (is_variable(pattern) && variable.get_name() === pattern.get_name()) { 71 | return true; 72 | } 73 | else if (is_variable(pattern) || is_atom(pattern)) { 74 | return false; 75 | } 76 | else if (is_array(pattern)) { 77 | for (i = 0; i < pattern.length; i += 1) { 78 | if (occurs(variable, pattern[i])) { 79 | return true; 80 | } 81 | } 82 | return false; 83 | } 84 | else if (is_object(pattern)) { 85 | for (key in pattern) { 86 | if (pattern.hasOwnProperty(key)) { 87 | if (occurs(variable, pattern[key])) { 88 | return true; 89 | } 90 | } 91 | } 92 | return false; 93 | } 94 | } 95 | 96 | function get_binding(variable, substitution) { 97 | var binding; 98 | 99 | if (substitution.hasOwnProperty(variable.get_name())) { 100 | binding = {}; 101 | binding[variable.get_name()] = substitution[variable.get_name()]; 102 | } 103 | return binding; 104 | } 105 | 106 | function add_substitution(variable, pattern, substitution) { 107 | substitution[variable.get_name()] = pattern; 108 | return substitution; 109 | } 110 | 111 | function match_var(variable, pattern, substitution) { 112 | var binding; 113 | 114 | // don't match a variable with another (or itself) 115 | if (is_variable(pattern) && is_variable(variable)) { 116 | return false; 117 | } 118 | // if the variable or pattern is a wildcard we return without binding 119 | else if (is_wildcard(variable) || is_wildcard(pattern)) { 120 | return substitution; 121 | } 122 | // if the variable has a type which doesn't match the type of the pattern, 123 | // we return false (no match) 124 | else if (variable.get_type() !== false && variable.get_type() !== pattern.constructor) { 125 | return false; 126 | } 127 | // otherwise we try to bind the pattern to the variable 128 | else { 129 | binding = get_binding(variable, substitution); 130 | // if it's already bound we call unify again to resolve any variables inside 131 | // the binding. 132 | if (binding) { 133 | return unify_aux(binding[variable.get_name()], pattern, substitution); 134 | } 135 | if (occurs(variable, pattern)) { 136 | return false; 137 | } 138 | else { 139 | return add_substitution(variable, pattern, substitution); 140 | } 141 | } 142 | } 143 | 144 | function unify_object(pattern1, pattern2, substitution) { 145 | var has_var, key, c1, c2; 146 | 147 | // if the two objects have different constructors we also consider 148 | // them unequal. This prevents for example: 149 | // Point(x, y) -> {x: x, y: y} and Vertex(x, y) -> {x: x, y: y} 150 | // to be regarded as the same object. 151 | if ((pattern1.constructor && pattern2.constructor) && pattern1.constructor !== pattern2.constructor) { 152 | return false; 153 | } 154 | 155 | has_var = pattern2.hasOwnProperty("_"); 156 | c1 = c2 = 0; 157 | 158 | for (key in pattern1) { 159 | if (pattern1.hasOwnProperty(key)) { 160 | if (key !== "_") { 161 | if (pattern2.hasOwnProperty(key)) { 162 | if (unify_aux(pattern1[key], pattern2[key], substitution) === false) { 163 | return false; 164 | } 165 | } 166 | else if (!pattern2.hasOwnProperty(key) && !has_var) { 167 | return false; 168 | } 169 | } 170 | c1 += 1; 171 | } 172 | } 173 | 174 | has_var = pattern1.hasOwnProperty("_"); 175 | 176 | for (key in pattern2) { 177 | if (pattern2.hasOwnProperty(key)) { 178 | if (key !== "_") { 179 | if (pattern1.hasOwnProperty(key)) { 180 | if (unify_aux(pattern1[key], pattern2[key], substitution) === false) { 181 | return false; 182 | } 183 | } 184 | else if (!pattern1.hasOwnProperty(key) && !has_var) { 185 | return false; 186 | } 187 | } 188 | c2 += 1; 189 | } 190 | } 191 | 192 | if (c1 === 0 && c2 === 0) { 193 | return substitution; 194 | } 195 | if (c1 === 0 || c2 === 0) { 196 | return false; 197 | } 198 | return substitution; 199 | } 200 | 201 | function unify_array(pattern1, pattern2, substitution) { 202 | var i; 203 | if (pattern1.length === pattern2.length) { 204 | for (i = 0; i < pattern1.length; i += 1) { 205 | if (unify_aux(pattern1[i], pattern2[i], substitution) === false) { 206 | return false; 207 | } 208 | } 209 | } else { 210 | return false; 211 | } 212 | return substitution; 213 | } 214 | 215 | function unify_aux(pattern1, pattern2, substitution) { 216 | if (substitution === false) { 217 | return false; 218 | } 219 | else if (is_variable(pattern1) || is_wildcard(pattern1)) { 220 | return match_var(pattern1, pattern2, substitution); 221 | } 222 | else if (is_variable(pattern2) || is_wildcard(pattern2)) { 223 | return match_var(pattern2, pattern1, substitution); 224 | } 225 | else if (is_atom(pattern1)) { 226 | if (pattern1 === pattern2) { 227 | return substitution; 228 | } 229 | else { 230 | return false; 231 | } 232 | } 233 | else if (is_atom(pattern2)) { 234 | return false; 235 | } 236 | else if (is_array(pattern1) && is_array(pattern2)) { 237 | return unify_array(pattern1, pattern2, substitution); 238 | } 239 | else if (is_object(pattern1) && is_object(pattern2)) { 240 | return unify_object(pattern1, pattern2, substitution); 241 | } 242 | } 243 | 244 | function visit_pattern(pattern, visitor) { 245 | var key, i, value; 246 | 247 | if (is_variable(pattern)) { 248 | return (visitor.hasOwnProperty('variable') && visitor.variable(pattern)) || pattern; 249 | } 250 | else if (is_wildcard(pattern)) { 251 | return (visitor.hasOwnProperty('wildcard') && visitor.wildcard()) || pattern; 252 | } 253 | else if (is_function(pattern)) { 254 | return (visitor.hasOwnProperty('func') && visitor.func(pattern)) || pattern; 255 | } 256 | else if (is_object(pattern)) { 257 | for (key in pattern) { 258 | if (pattern.hasOwnProperty(key) && visitor.hasOwnProperty('object') && key !== _) { 259 | value = visitor.object(key, pattern[key]); 260 | pattern[key] = visit_pattern(value, visitor); 261 | } 262 | else if (pattern.hasOwnProperty(key) && key !== _) { 263 | pattern[key] = visit_pattern(pattern[key], visitor); 264 | } 265 | } 266 | return pattern; 267 | } 268 | else if (is_array(pattern)) { 269 | for (i = 0; i < pattern.length; i += 1) { 270 | pattern[i] = visit_pattern(pattern[i], visitor); 271 | } 272 | return pattern; 273 | } 274 | else { 275 | return (visitor.hasOwnProperty('atom') && visitor.atom(pattern)) || pattern; 276 | } 277 | } 278 | 279 | return { 280 | unify : function (pattern1, pattern2) { 281 | return unify_aux(pattern1, pattern2, {}); 282 | }, 283 | visit_pattern : function (pattern, visitor) { 284 | return visit_pattern(pattern, visitor); 285 | }, 286 | variable : variable, 287 | _ : _ 288 | }; 289 | }()); 290 | 291 | module.exports = unification; 292 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Bram Stein (http://www.bramstein.com)", 3 | "name": "junify", 4 | "description": "JavaScript implementation of the unification algorithm", 5 | "keywords": ["unification", "junify", "pattern matching", "expression matching"], 6 | "version": "1.0.2", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/bramstein/junify.git" 10 | }, 11 | "main": "lib/unification.js", 12 | "engines": { 13 | "node": "*" 14 | }, 15 | "scripts": { 16 | "test": "vows" 17 | }, 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "vows": "*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/unification-test.js: -------------------------------------------------------------------------------- 1 | var junify = require('../lib/unification'), 2 | vows = require('vows'), 3 | assert = require('assert'), 4 | $ = junify.variable, 5 | _ = junify._, 6 | u = junify.unify; 7 | 8 | vows.describe('Junify').addBatch({ 9 | 'unify': { 10 | topic: function () { 11 | return u; 12 | }, 13 | 'array': function (u) { 14 | assert.ok(u([], [])); 15 | assert.ok(u([1],[1])); 16 | assert.isFalse(u([1],[2])); 17 | assert.isFalse(u([], [1, 2, 3])); 18 | assert.isFalse(u([1], [1, 2, 3])); 19 | assert.ok(u([1, [3, 4], 5], [1, [3, 4], 5])); 20 | }, 21 | 'object': function (u) { 22 | assert.ok(u({}, {})); 23 | assert.ok(u({hello: 'world'}, {hello: 'world'})); 24 | assert.ok(u({hello: 'world', key: 'value'}, {key: 'value', hello: 'world'})); 25 | assert.isFalse(u({hello: 'world', key: 'value'}, {hello: 'world'})); 26 | assert.isFalse(u({hello: 'world'}, {hello: 'world', key: 'value'})); 27 | }, 28 | 'function': function(u) { 29 | var f = function() {}, 30 | g = function() {}; 31 | 32 | assert.ok(u(f, f)); 33 | assert.isFalse(u(f, g)); 34 | }, 35 | 'variable': function (u) { 36 | assert.equal(u($('a'), 1).a, 1); 37 | assert.equal(u(1, $('a')).a, 1); 38 | assert.equal(u({hello: $('a')}, {hello: 'world'}).a, 'world'); 39 | 40 | assert.isFalse(u([$('a'), $('a')], [1, 2])); 41 | assert.isFalse(u($('a'), $('a'))); 42 | assert.isFalse(u([$('a'), 2], [1, $('a')])); 43 | assert.equal(u([[$('a')],$('b')], [[1],2]).a, 1); 44 | assert.equal(u($('n'), function () { return 10; }()).n, 10); 45 | }, 46 | 'type': function (u) { 47 | assert.ok(u($('d', Date), new Date())); 48 | assert.ok(u($('d', String), 'hello')); 49 | assert.ok(u($('d', Function), function() {})); 50 | assert.ok(u(String, _)); 51 | }, 52 | 'wildcard': function (u) { 53 | assert.ok(u(_, _)); 54 | assert.ok(u(_, 1)); 55 | assert.ok(u(1, _)); 56 | }, 57 | 'wildcard array': function (u) { 58 | assert.ok(u(_, [1, 2])); 59 | assert.ok(u([1, 2], _)); 60 | }, 61 | 'wildcard variable': function (u) { 62 | assert.isUndefined(u($('a'), _).a); 63 | assert.isUndefined(u(_, $('a')).a); 64 | }, 65 | 'wildcard object': function (u) { 66 | assert.ok(u({_ : _}, {_ : _})); 67 | assert.isFalse(u({_ : _}, {})); 68 | assert.isFalse(u({}, {_ : _})); 69 | 70 | assert.ok(u(_, {hello: 'world'})); 71 | assert.ok(u({hello: 'world'}, _)); 72 | assert.ok(u({hello: 'world', _: _}, {key: 'value', _ : _ })); 73 | assert.ok(u({hello: 'world', _: _}, {key: 'value', hello: 'world'})); 74 | assert.ok(u({hello: 'world', key: _}, {key: 'value', hello: 'world'})); 75 | } 76 | } 77 | }).export(module); 78 | --------------------------------------------------------------------------------