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:
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:
var output = {
x: 23,
y: 42,
tooltip: "Bob",
original: ... // our original object
};
It is easy to write an input pattern:
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.
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, andunify.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 asprocessObject()
.registry
is an array similar tounify.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
, andRegExp
. - The default
registry
is exposed asclone.registry
.
- The default
filters
is an array similar tounify.filters
also described in heya-unify: custom unification. It contains filter functions.- The default
filters
is empty. - The default
filters
is exposed asclone.filters
.
- The default
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:
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.
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!
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.
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}"
, wherename
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}"
.
- Substitutions are indicated by a code like this:
env
is an environment object returned byunify()
.
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:
- Unification for JS — the introduction.
- When to unify in JS — when to unify with examples.
- heya-unify: custom unification — simple ways to customize unification for a specific project.
- heya-unify: incomplete objects — write incomplete object patterns like a pro!
- heya-unify: back to JS — this post.
Thank you for your help and suggestions!
Attributions
This post uses image by Psycho Delia under Creative Commons License.