Skip to content

Axes as relations

An axis is a relation between the focus and a list of target keys. The body of the axis evaluates once at each target, and the result is a list. A reduction then makes one value from the list.

Select an origin, an axis and a reduction below. The matrix shows the relation for each origin. The arrows go from the origin to its targets, with the distance at each target.

Axes are relationsaxis
511.059.22
The relation. A row is an origin, a column is a target.
ABCD
·●●●
●·●●
●●·●
●●●·
reduce

Result at A: 5

root.from("position")
  .others((e) => e._.subtract("position")._.length())
  .min()
Axis Builder Targets Error
all .each(body) in a chain, .all() at the top each key of the space not applicable
others .others(body) each key except the focus not applicable
other each(axes.other, body) in the IR the other key of a pair #REF! if the space does not have two keys
neighbors .neighbors(4, body) or .neighbors(8, body) the cells next to the focus in a grid #REF! outside a grid
where the option where of an axis the targets of the inner axis where the test gives true an error item for a test that fails

Each axis method of the builder takes a body function and an optional { where } test. The result is a list chain, with the reductions in the last section.

Inside an axis, the focus is the target, and the origin stays the same. A value that the program binds outside the axis keeps its value. Thus a base value from the origin and a field of the target meet in one body.

const nearest = root.from("position") // the base value, at the origin
.others((e) => e._.subtract("position") // "position" reads at each target
._.length())
.min(); // the distance to the nearest other box
nearest.at("A"); // => some(5)

The body function gets a chain e that starts with the base value. The builder binds the base value to a name before the axis. The IR shows it:

app("min",
let_({ $0: ref("position") },
each(axes.others,
app("length", app("subtract", v("$0"), ref("position"))))));

The binding $0 evaluates once, at the origin. The reference ref("position") in the body reads at each target. In the trace, the event of $0 has the focus B, but its value is the position of A.

The first versions of Vex kept the focus in one shared mutable value. The axis loop moved the focus to each target, and the reference to the origin moved the same focus back. Thus the base value and the target field read the same record, and each distance was 0.

The spec example EXAMPLE.NEAREST expects the distance to the nearest other record. The old peers() gave 0 for each peer, in each version (defect V-042). Vex 1.0 keeps two positions, the origin and the focus. Both are values, and nothing changes them in place.

The program below adds the offsets from the origin to each other box, with the domain op add. Move the slider to see the body at each target.

The sum of the offsets to the othersorigin
root.from("position")
  .others((e) => e._.subtract("position"))
  .reduce("add")
step 14 of 14

Result at A: (-17, -13)

originABCD
result(-17, -13)(-1, -1)(27, -9)(-9, 23)

At the origin A, the offsets are (-4, -3), (-11, -1) and (-2, -9). Their sum is (-17, -13). Drag a box to change the offsets.

The option where keeps only the targets where a test gives true. The test evaluates at each target, with the same base value as the body.

const near = root.from("position")
.others((e) => e._.subtract("position")._.length(), {
where: (e) => e._.subtract("position")._.length()._.lt(8),
})
.count(); // the number of other boxes nearer than 8
near.at("A"); // => some(1)

A test that fails, or that does not give a boolean, gives an error item for its target. The “near” axis of the explorer above uses this test, with a radius that you can change.

Reduction Items Empty list
count() any 0
sum() numbers 0
mean(), min(), max() numbers #N/A
any(), all(), none() booleans false, true, true
first() any #N/A
values() any an empty array
reduce(op) values of a domain with the op the identity of the op, or #N/A

An item of the wrong kind gives #VALUE!. The types check this too: min() exists only on a list of numbers.

A reduction is lenient by default: it skips error items. With { strict: true }, the first error item is the result.

// Some records have no field "mass".
root.from("mass").others((e) => e.from("mass")).max(); // the largest mass of the others
root.from("mass").others((e) => e.from("mass")).max({ strict: true }); // #N/A if one other has no mass

A tree space has a parent for each key that is not a root: space.tree(records, parents). The move parent() reads at the parent, and four axes follow the relations of the tree:

Axis Targets
children the children of the focus, in key order
ancestors the parent first, the root last
descendants each key under the focus, depth first, with each parent before its children
siblings the other children of the parent, or the other roots for a root

The cost of a team is the salary of a person plus the salaries of the descendants. The depth of a person is the number of ancestors:

const org = vex(NumDomain).over(space.tree(
{
ceo: { name: "Ada", salary: 300 },
cto: { name: "Bo", salary: 200 },
dev: { name: "Cy", salary: 100 },
cfo: { name: "Ed", salary: 190 },
},
{ cto: "ceo", dev: "cto", cfo: "ceo" },
));
const team = org.from("salary")._.add(org.start(0).descendants((d) => d.from("salary")).sum());
team.all().values(); // => [790, 300, 100, 190]
org.start(0).ancestors((a) => a).count().at("dev"); // => some(2)
org.start(0).parent().from("name").at("dev"); // => some("Bo")
org.start(0).siblings((s) => s.from("name")).values().at("cto"); // => some(["Ed"])

A root has no parent, so parent() at a root gives #REF!. In a space that is not a tree, parent() and the four axes give #REF!. Graph has a $ancestor reference for the same need.

Select a person to move the origin, and select an axis:

An organization chart as a tree space
Ada300Bo200Ed190Cy100Di110Fa120Gu90

descendants of Bo: Cy, Di, Fa

The cost of the team of Bo: 530. Select a person to move the origin.

Three laws of the spec connect the axes. @mark1russell7/vex-testkit checks each law with random programs and random spaces.

  • AXIS.OTHERS-UNION: the targets of others and the focus are the targets of all.
  • AXIS.OTHER-TWICE: in a pair, the address [other, other] points to the focus.
  • AXIS.EXTEND: for an expression that does not read the origin, the item at key k of each(all, e) equals e evaluated at the origin k.

Refer to the Store comonad for the theory behind these laws.