Eugene's Blog

I can't believe it's blog!

heya-unify: back to JS

As programmers we rarely have a luxury to write a project from scratch. Usually we have to retrofit existing projects with all cool things we need. If a new component, or a library we want to use introduces new concepts that bleed outside its boundary, we have a “culture clash”, when old code is unaware about new concepts have to work with it anyhow. Sometimes the clash is so bad that we have to give up on using shiny new things, or have to significantly rework their code, which requires time and efforts we cannot afford.

This situation calls for elaborate translation facilities between “old” and “new” worlds, or our new component/library should be aware of the problem, and provide necessary hooks, and utilities to adapt for existing projects.

heya-unify was designed from the ground up to support such paradigm. We already saw in heya-unify: custom unification how to unify our custom objects, or modify a unification of existing objects. This post deals with results of unification by cleaning them from unification variables, so they can be used in parts, which are not aware of how those results were produced.

Looking at the 1st, 2nd, 3rd, and 4th parts of the series is recommended before diving into details.

Back to JavaScript

Usually we use unify() to leverage unification variables, which means that our results can contain variables (essentially object references). Usually by that time all variables are bound, yet our code may not be aware of their existence. How to deal with it?

A related consideration: in most cases we don’t need pure variables, but some object structures may contain them. Frequently we use patterns to parse objects, and hold parsed values in variables, and other patterns to create new objects from parsed objects.

For example, we have objects like these:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var unify      = require("heya-unify"), // for node.js
    variable   = unify.variable,
    preprocess = require("heya-unify/utils/preprocess"),
    ref        = require("heya-unify/unifiers/ref");

var input = {
  name: "Bob",
  pos: {
    x: 23,
    y: 42
    // may have more properties
  }
  // may have more properties
};

But we want to transform it to something like that:

1
2
3
4
5
6
var output = {
  x: 23,
  y: 42,
  tooltip: "Bob",
  original: ... // our original object
};

It is easy to write an input pattern:

1
2
3
4
5
6
7
8
9
10
11
12
var self = variable(),
    x    = variable(),
    y    = variable(),
    name = variable();

var iPattern = preprocess(ref(self, {
  name: name,
  pos: {
    x: x,
    y: y
  }
}), true);

But what now? How can we assemble and pass the results, so our plain JavaScript code can deal with it, without going into details of unification artifacts, like variables?

clone()

We can clone an object containing variables providing that it doesn’t have circular references. While cloning all variables will be resolved and replaced with their values. This is a non-destructive operation, which keeps an original result object intact.

Despite its internal knowledge of variables, clone() can be used on any type of objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
var clone = require("heya-unify/utils/clone");

var oPattern = {
  x: x,
  y: y,
  tooltip:  name,
  original: self
};

var env = unify(iPattern, obj), result;
if(env){
  result = clone(oPattern, env)
}

As you can see our code is purely declarative and simple to understand: we described our input and output as JavaScript literals, then called unify(), then conditionally called clone().

clone() accepts three arguments:

  • source is an object to be cloned.
    • All bound variables will be resolved and replaced with their values.
    • All unbound variables, unify.any, and wrapped objects will be passed as is.
  • env is an environment to be used to resolve variables.
  • opt is an optional configuration object, which may contain following optional properties:
    • processObject is a procedure to handle simple objects, and unify.any. It takes two arguments:
      • val is a value to be cloned.
      • stack is an array, which is used as a cloning stack. Objects place on it will be cloned. This is a provision to clone sub-objects.
        • We can use commands to notify us when sub-objects were cloned. Commands are described below.
    • processOther is a procedure to clone non-object values, usually by copying. It takes the same arguments as processObject().
    • registry is an array similar to unify.registry described in heya-unify: custom unification. It contains constructor functions with corresponding clone procedures.
      • The default registry contains handlers for following objects: Array, unify.Variable, Date, and RegExp.
      • The default registry is exposed as clone.registry.
    • filters is an array similar to unify.filters also described in heya-unify: custom unification. It contains filter functions.
      • The default filters is empty.
      • The default filters is exposed as clone.filters.

Both clone procedures of registry and filter functions of filters are called with two arguments: a value to be inspected/cloned, and a context object. The most important parts of the context are two arrays used as stacks: stack and stackOut. The former is used to accept more values to be cloned, while the latter is used to keep already cloned values.

Remember that preprocess() doesn’t know how to handle cloning of custom objects, yet we can easily add this functionality to clone(). In the example we will reuse Unifier and Person described in heya-unify: custom unification:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var Unifier = unify.Unifier;

// let's add cloning to our Unifier interface:
Unifier.prototype.clone = function(){
  // default implementation throws
  throw Error("clone() method should be implemented!");
};

// ...

var dcl = require("dcl");

var Person = dcl(Unifier, {
  constructor: function(name, dob, tin){
    this.name = name;
    this.dob  = dob;  // date of birth
    this.tin  = tin;  // tax identification number
  },
  unify: function(val, ls, rs, env){
    return val instanceof Person && this.tin === val.tin;
  },
  clone: function(context){
    context.stackOut.push(new Person(this.name, this.dob, this.tin));
  }
});

// now Person objects can be cloned properly by clone()

var result = clone(oPattern, env, {
  registry: clone.registry.concat(
    Unifier,
    function(val, context){
      val.clone(context);
    }
  )
});

assemble()

A frequent operation is to dereference unification variables substituting them with their values. This way we don’t need to “explain” to our code how to handle them. While we may go over an object graph, while modifying it, the less invasive procedure is to clone parts that contain variables. In this case all objects without variables in them are used unmodified, which makes the whole process faster, and easier on memory.

assemble() is a cousin of clone(), which clones conservatively. Only branches with variables in them will be cloned, and modified leaving the original result object unchanged. If a source object does not have any variables in it, it will be left as is.

assemble() accepts the same parameters as clone(). Virtually it can be used as a drop-in replacement for it. It is a non-destructive operation, which keeps an original object intact.

1
2
3
4
5
6
7
8
9
10
11
12
13
var assemble = require("heya-unify/utils/assemble");

var oPattern = {
  x: x,
  y: y,
  tooltip:  name,
  original: self
};

var env = unify(iPattern, obj), result;
if(env){
  result = assemble(oPattern, env)
}

deref()

Obviously, if we have absolutely no other use for results, we can replace variables destructively in place. While it is fast, and conserves memory, it cannot be applied to object with circular references, and may affect any bound unification variables defined in the current environment. Not only that, it will eliminate all variables from a pattern as well making it non-reusable. Do not use deref() with static patterns!

deref() belongs to the same group as assemble(), and clone(). It accepts the same parameters as clone(). Virtually it can be used as a drop-in replacement for it, but beware of side effects!

1
2
3
4
5
6
7
8
9
10
11
12
13
var deref = require("heya-unify/utils/deref");

var oPattern = {
  x: x,
  y: y,
  tooltip:  name,
  original: self
};

var env = unify(iPattern, obj), result;
if(env){
  result = deref(oPattern, env)
}

replace()

This function is a simple templating facility, which can embed unification variable values by name in a string. It is especially useful for debugging, or when a legacy code expects strings rather than objects.

1
2
3
4
5
6
7
8
var replace = require("heya-unify/utils/replace");

var env = unify({name: "Jill", age: 25},
  {name: variable("name"), age: variable("age")});
if(env){
  console.log(replace("${name} is ${age}", env));
  // prints: Jill is 25
}

replace() takes two arguments:

  • tmpl is a template string.
    • Substitutions are indicated by a code like this: "${name}", where name is a variable’s name.
    • Only explicitly named variable can be used in a pattern.
    • Pattern with more than one dollar sign in a row reproduces itself reducing a number of dollar signs by one. Example: "$$${x} $${y}" will produce a string "$${x} ${y}".
  • env is an environment object returned by unify().

Summary

heya-unify provides a set of utilities to interface with plain JavaScript code, which is not aware of unification concepts. It makes it a viable solution for retrofitting legacy projects.

This is the last post in the series, which presents heya-unify library.

Unification posts

All installments will be posted in this list as soon as they go online:

Thank you for your help and suggestions!

Attributions

This post uses image by Psycho Delia under Creative Commons License.

Comments