zippers

references:

Functional pearl - The zipper - GĂ©rard Huet 1997


zipper is way to navigate a data structure keeping a focal point.

the main operations are: left, right, up and down.

they are very handy because when a point of interest is found, you can edit in-place with zero cost.

let's right a simple zipper for an array (it will be a mutable structure for convinience).

a array zipper will have 3 properties:

  • left

keep the elements we walked, from start to focal point.

  • at

the current focusing element.

  • right

keep the elements from focal point to right.

function ArrayZipper(array) {
  this._left = [];
  this._at = array[0];
  this._right = array.slice(1);
}

ArrayZipper.of = (array) => new ArrayZipper(array);

const azp = ArrayZipper.prototype;

// just get the focusing element
azp.at = function() {
  return this._at;
};

// replace the focusing element with a new value
azp.edit = function(v) {
  this._at = v;
};

with our zipper, we can walk right...

azp.right = function() {
  this._left.push(this._at);
  this._at = this._right.shift();
};

let z = ArrayZipper.of([1, 2, 3]);

z.right(), z;
// ArrayZipper { _left: [ 1 ], _at: 2, _right: [ 3 ] }

z.right(), z;
// ArrayZipper { _left: [ 1, 2 ], _at: 3, _right: [] }

or walking left...

azp.left = function() {
  this._right = [this._at].concat(this._right);
  this._at = this._left.pop();
};

// walking from our previous defined zipper

z.edit(10), z;
// ArrayZipper { _left: [ 1, 2 ], _at: 10, _right: [] }

z.left(), z
// ArrayZipper { _left: [ 1 ], _at: 2, _right: [ 10 ] }

z.left(), z
// ArrayZipper { _left: [], _at: 1, _right: [ 2, 10 ] }

but it can also conform to other interfaces.

azp.map = function(f) {
  this._left = this._left.map(f);
  this._at = f(this._at);
  this._right = this._right.map(f);
  return this;
};

z
// ArrayZipper { _left: [], _at: 1, _right: [ 2, 10 ] }

z.map(x => x + 1), z;
// ArrayZipper { _left: [], _at: 2, _right: [ 3, 11 ] }

focus (just like find, but to look at an element).

azp.focus = function(f) {
  const tmp = this._left.concat(this._at).concat(this._right);
  const position = tmp.findIndex(f);
  this._left = tmp.slice(0, position);
  this._at = tmp[position];
  this._right = tmp.slice(position + 1, tmp.length);
};

z
// ArrayZipper { _left: [], _at: 2, _right: [ 3, 11 ] }

z.focus(v => v == 11), console.log(z);
// ArrayZipper { _left: [ 2, 3 ], _at: 11, _right: [] }

or...find, find...

azp.find = function(f) {
  return this._left.find(f) ||
	(f(this._at) && this._at) ||
	this._right.find(f) ||
	null;
};

z.find(v => v == 11)
// 11