heya-unify: incomplete objects
Incomplete objects allow us to concentrate on important properties of JavaScript objects ignoring the rest: we don’t need to specify every single property, and we can deal with cyclical graphs.
Incomplete arrays is a complimentary feature to inspect only the first few array items.
Both features are very useful for patterns, and heya-unify provides rich facilities to automate creating incomplete objects: they can be marked up explicitly on per-instance basis, recursively with a special utility, and we can specify how to deal with objects by default during unification.
Looking at the 1st, 2nd, and 3rd parts of the series is recommended before diving into details.
Incomplete objects
From the first part of series we know that we can unify regular objects and arrays using two algorithms: “exact”, when all properties should match, and “open”, when listed properties should match, while the rest is ignored. It gives us flexibility to concentrate only on significant properties bypassing various additions, which are very common in JavaScript, and resolving potential circular references.
By default all objects and arrays are assumed to be exact, but there are several ways to deal with open objects.
Explicit mark up
As you know already we can use a special function to mark an object as open:
var unify = require("heya-unify"); // for node.js
var open = unify.open;
var pattern = open({
position: {
x: 1,
y: 2
}
});
open()
is not recursive: only the immediate object is marked, all sub-objects will be exact
unless explicitly marked.
Explicit recursive mark up
Frequently we want all objects of our pattern to be open. Marking them up one by one gets old pretty quick. Precisely for such case there is a utility that handles it:
var preprocess = require("heya-unify/utils/preprocess");
var pattern = {
position: {
x: 1,
y: 2
}
};
pattern = preprocess(pattern, true, false);
preprocess()
accepts four arguments:
source
is an object to be processed.nonExactObject
is a flag. If it is truthy, all objects excluding arrays, standard dates, regular expressions, and unification variables, are marked as open. Otherwise they are unchanged.nonExactArrays
is a flag. If it is truthy, all arrays are marked as open. Otherwise they are unchanged.opt
is an optional configuration object. It can be used to specify special treatment of custom objects. Following optional properties can be specified:context
is an arbitrary object, which will be passed to all other functions. It will be pre-populated with following properties:stackOut
is an array used as stack to push converted objects. Ultimately all object processing procedures should push results there.wrapArray(val)
is a function, which makes arrays open (see “Internal mechanics” below). By default it has a falsy value, and not used.wrapObject(val)
is a function, which makes simple objects open (see “Internal mechanics” below). By default it has a falsy value, and not used.
processObject(val, context)
is called for every simple object.processOther(val, context)
is called for every non-object as defined bytypeof
, ornull
.registry
is an array similar tounify.registry
:- All even items are assumed to be constructor functions. Objects are compared against them with
instanceof
. - Corresponding odd items are transformation procedures, which the same signature as
processObject()
above.
- All even items are assumed to be constructor functions. Objects are compared against them with
filters
is an array of functions. Each function takes the same parameters asprocessObject()
above, and returns a truthy value, if it can handle an object, or a falsy value otherwise.
It is not necessary to specify registry
and filters
directly in opt
. Their default values are exposed
as properties on preprocess()
, and can be modified in place for lasting changes.
preprocess()
returns a clone of source
leaving source
unmodified.
Let’s exclude instances of Person
from preprocessing as an example on customization:
preprocess.registry.push(
Person,
function(val, context){
// let's output it unmodified
context.stackOut.push(val);
}
);
pattern = preprocess(pattern, true, true);
In the example above all Person
instances are passed to a generated tree unmodified. It makes
Person
an opaque object, which has no visible references we can follow. In many cases this is
a behavior we want.
Things to be aware of:
- Cycles can be broken by omitting back references using incomplete patterns.
- Unification variables can help with making sure that references are correctly pointed.
- Registry and filters can be used to properly preprocess custom objects.
Internal mechanics
unify.open()
avoids modifying our objects in any way. Instead it wraps an open object in a wrapper
object with two properties:
object
is a reference to an original object.type
is a string. It can be either “open” or “exact”.- While “exact” configuration is supported, it is rarely used.
We can always check for a wrapper using a helper function:
var isWrapped = unify.isWrapped,
isOpen = unify.isOpen;
var exactArray = [42],
openArray = open(exactArray);
console.log(isWrapped(exactArray)); // false
console.log(isOpen(exactArray)); // false
console.log(exactArray.length); // 1
console.log(exactArray[0]); // 42
console.log(isWrapped(openArray)); // true
console.log(isOpen(openArray)); // true
console.log(openArray.type); // open
console.log(openArray.object === exactArray); // true
Treat all objects as open
This is the third option: we can tell unify()
to treat all objects/arrays as open. In order to do that
we should pass an environment (even empty one), which may have following properties defined:
objectType
is a flag. If it is truthy, all simple objects are treated as open.arrayType
is a flag. If it is truthy, all arrays are treated as open.
Wrapped objects are handled according to their type regardless of any flags.
Example:
var Env = unify.Env;
var env = new Env();
env.objectType = true;
// now all simple objects are open on both sides,
// while arrays are still exact.
env = unify({a: 1, b: 2}, {a: 1, c: 3}, env);
How are two open objects unified in the example above? Will it fail? No.
Only common properties will be unified (in our case it is a
), the rest will be
ignored (b
and c
). So the example shows a successful unification.
Note: the case when both objects should be treated as open is pretty rare. In most cases
preprocess()
or explicit open()
are the right solutions.
Summary
heya-unify’s built-in support for incomplete arrays and objects simplifies greatly writing sophisticated patterns. Incomplete patterns, variable, and custom unification make heya-unify flexible, and powerful. It was designed to use in new and existing projects.
But how to interface to an existing code, which has no idea about unification? The problem is that a result of unification, depending on used patterns, may contain unification variables, which requires a certain knowledge on how to dereference them. heya-unify has a set of utilities to reconstruct “pure” JavaScript objects, which support custom cloning, and can be used even without unification. These topics will be discussed in the next blog post.
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 — this post.
- heya-unify: back to JS — adapt unification results for legacy code.
Thank you for your help and suggestions!
Attributions
This post uses image by Martin Eckert under Creative Commons License.