Eugene's Blog

I can't believe it's blog!

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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
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 by typeof, or null.
    • registry is an array similar to unify.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.
    • filters is an array of functions. Each function takes the same parameters as processObject() 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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
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:

Thank you for your help and suggestions!

Attributions

This post uses image by Martin Eckert under Creative Commons License.

Comments