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.
| A | B | C | D | |
|---|---|---|---|---|
| · | ● | ● | ● | |
| ● | · | ● | ● | |
| ● | ● | · | ● | |
| ● | ● | ● | · |
Result at A: 5
root.from("position")
.others((e) => e._.subtract("position")._.length())
.min()The axes
Section titled “The axes”| 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.
Origin and target
Section titled “Origin and target”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.
Why the old focus failed
Section titled “Why the old focus failed”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.
Step through an axis
Section titled “Step through an axis”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.
root.from("position")
.others((e) => e._.subtract("position"))
.reduce("add")Result at A: (-17, -13)
| origin | A | B | C | D |
|---|---|---|---|---|
| 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.
Filter with where
Section titled “Filter with where”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.
Reduce the list
Section titled “Reduce the list”| 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 othersroot.from("mass").others((e) => e.from("mass")).max({ strict: true }); // #N/A if one other has no massA 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:
descendants of Bo: Cy, Di, Fa
The cost of the team of Bo: 530. Select a person to move the origin.
The laws of the axes
Section titled “The laws of the axes”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
othersand the focus are the targets ofall. - 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
kofeach(all, e)equalseevaluated at the origink.
Refer to the Store comonad for the theory behind these laws.