Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Select

Select lets us create a graph node with multiple possible output paths that will choose one path for each value based on a set of conditions. Kind of like,

                     ┌─────────────────────> if foo > 0 => ...
                     │
                     │
ref(foo) ──> select ─┼─────────────────────> if foo < 0 => ...
                     │
                     │
                     └─────────────────────> otherwise => ...

is written as

select foo {
  n if n > 0 => ...,
  n if n < 0 => ...,
  n => ...
}

select takes an expression as an argument and then evaluates one or more “arms”. Each arm consists of an optional type predicate, a destructuring pattern, and an optional guard clause. If the type predicate matches, the pattern matches, and the guard evaluates to true then the arm is “selected”. Only one arm may be selected at a time, the arms are evaluated in lexical order, and first arm to be selected is chosen as the one and only selected arm.

The code on the right side of the selected arm is the only code that is evaluated by select, all other code is “asleep”, it will not be evaluated until it is selected (and if it has netidx subscriptions or published values they will be unsubscribed and unpublished until it is selected again).

Like every other expression, a select updates whenever any of its inputs update: a new value of the argument, a new value read by a guard, or a new value produced by the selected arm’s body all cause the select to produce its current value — even if the same arm stays selected and the value is unchanged. If you want updates only when the value actually changes, wrap the argument (or the whole select) in uniq.

Matching Types

Consider we want to select from a value of type [Array<i64>, i64, null],

let x: [Array<i64>, i64, null] = null;
x <- sys::time::timer(duration:1.s, false) ~ [1, 2, 3, 4, 5];
x <- sys::time::timer(duration:2.s, false) ~ 7;
select x {
  Array<i64> as a => array::fold(a, 0, |s, x| s + x),
  i64 as n => n,
  null as _ => 42
}

This program will print 42, 15, 7 and then wait. The compiler will check that you have handled all the possible cases. If we remove the null case from this select we will get a compile error.

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    missing match cases type mismatch [i64, Array<i64>] does not contain [[i64, null], Array<i64>]

If you read this carefully you can see that the compiler is building up a set of types that we did match, and checking that it contains the argument type. This goes both ways, a match case that could never match is also an error.

let x: [Array<i64>, i64, null] = null;
x <- sys::time::timer(duration:1.s, false) ~ [1, 2, 3, 4, 5];
x <- sys::time::timer(duration:2.s, false) ~ 7;
select x {
  Array<i64> as a => array::fold(a, 0, |s, x| s + x),
  i64 as n => n,
  f64 as n => cast<i64>(n)?,
  null as _ => 42
}

Here we’ve added an f64 match case, but the argument type can never contain an f64 so we will get a compile error.

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    pattern f64 will never match null, unused match cases

The diagnostic message gives you an insight into the compiler’s thinking. What it is saying is that, by the time it’s gotten to looking at the f64 pattern, the only type left in the argument that hasn’t already been matched is null, and since f64 doesn’t unify with null it is sure this pattern can never match.

Guarded patterns can always not match because of the guard, so they do not subtract from the argument type set. You are required to match without a guard at some point. No analysis is done to determine if your guard covers the entire range of a type.

let x: [Array<i64>, i64, null] = null;
x <- sys::time::timer(duration:1.s, false) ~ [1, 2, 3, 4, 5];
x <- sys::time::timer(duration:2.s, false) ~ 7;
select x {
  Array<i64> as a => array::fold(a, 0, |s, x| s + x),
  i64 as n if n > 10 => n,
  null as _ => 42
}

This will fail with a missing match case because the i64 pattern is guarded and no unguarded pattern exists that matches i64.

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    missing match cases type mismatch [null, Array<i64>] does not contain [[i64, null], Array<i64>]

This is the same error you would get if you omitted the i64 match case entirely.

Matching Structure

The type predicate is optional in a pattern, and the more commonly used pattern is structural. Graphix supports several kinds of structural matching,

  • array slices
  • tuples
  • structs
  • variants
  • literals, ignore

NB: In most contexts you can match the entire value as well as parts of it’s structure by adding a v@ pattern before the pattern. You will see this in many of the examples.

Slice Patterns

Suppose we want to classify arrays that have at least two elements vs arrays that don’t, and we want to return a variant with a triple of the first two elements and the rest of the array or `Short with the whole array.

let a = [1, 2, 3, 4];
a <- [1];
a <- [5, 6];
select a {
  [x, y, tl..] => `Ok((x, y, tl)),
  a => `Short(a)
}

This program will print,

$ graphix test.gx
`Ok((1, 2, [3, 4]))
`Short([1])
`Ok((5, 6, []))

The following kinds of slice patterns are supported,

  • whole slice, with binds, or literals, e.g. [1, x, 2, y] matches a 4 element array and binds it’s 2nd and 4th element to x and y respectively.

  • head pattern, like the above program, e.g. [(x, y), ..] matches the first pair in an array of pairs and ignores the rest of the array, binding the pair elements to x and y. You can also name the remainder, as we saw, e.g. [(x, y), tl..] does the same thing, but binds the rest of the array to tl

  • tail pattern, just like the head pattern, but for the end of the array. e.g. [hd.., {foo, bar}] matches the last element of an array of structs with fields foo and bar, binding hd to the array minus the last element, and foo to field foo and bar to field bar.

Structure patterns (all of the different types) can be nested to any depth.

Slice patterns count toward exhaustiveness by LENGTH: a set of unguarded slice arms whose element patterns are all binds or _ covers the array when their lengths cover every possible length — which needs a head or tail (rest) pattern for the unbounded lengths, plus an exact-length arm for each length below the rest pattern’s minimum. So this select is exhaustive with no wildcard arm:

select a {
  [] => `Empty,
  [x, tl..] => `Cons(x, tl)
}

An arm carrying a guard, or an element pattern that can refute (a literal, a variant, a nested slice), only matches SOME arrays of its length, so it claims no coverage — the error says so when that is why a select falls short. Note also that a slice arm covers only arrays of its own inferred (or written) element type: if the scrutinee is a union of differently-typed arrays, each array member needs the whole length ladder to check out against every slice arm’s type.

Dead-arm checking is length-precise in the same way: an arm whose every matchable length is already matched by earlier covering slice arms can never run and is refused — [init.., y] after [x, tl..] is dead (both match every non-empty array), and a _ behind a complete ladder like the one above is dead exactly as it would be behind a full variant set. Order exact-length arms above the rest arm that subsumes them.

Tuple Patterns

Tuple patterns allow you to match tuples. Compared to slice patterns they are fairly simple. You must specify every field of the tuple, you can choose to bind it, or ignore it with _. e.g.

("I", "am", "a", "happy", "tuple", w, _, "patterns")

Struct Patterns

Struct patterns, like tuple patterns, are pretty simple.

  • { x, y } if you like the field names then there is no need to change them
  • { x: x_coord, y: y_coord } but if you need to use a different name you can
  • { x, .. } you don’t have to write every field

Consider

let a = {x: 54, y: 23};
a <- {x: 21, y: 88};
a <- {x: 5, y: 42};
a <- {x: 23, y: 32};
select a {
  {x, y: _} if (x < 10) || (x > 50) => `VWall,
  {y, x: _} if (y < 10) || (y > 40)  => `HWall,
  {x, y} => `Ok(x, y)
}

does some 2d bounds checking, and will output

$ graphix test.gx
`VWall
`HWall
`VWall
`Ok(23, 32)

You can replace y: _ with .. — a partial pattern names only the fields it needs, and the type checker completes the rest from the scrutinee’s type:

let a = {x: 54, y: 23};
a <- {x: 21, y: 88};
a <- {x: 5, y: 42};
a <- {x: 23, y: 32};
select a {
  {x, ..} if (x < 10) || (x > 50) => `VWall,
  {y, ..} if (y < 10) || (y > 40)  => `HWall,
  {x, y} => `Ok(x, y)
}
$ graphix test.gx
`VWall
`HWall
`VWall
`Ok(23, 32)

This works at any nesting depth — a partial pattern inside a variant payload (`Event({id, ..})) completes against that payload’s type. The one case that still needs help is a scrutinee union in which SEVERAL struct members carry all the named fields: the pattern is ambiguous, and the compiler asks you to annotate the member you mean:

S as {x, ..} if (x < 10) || (x > 50) => `VWall

Any type in scope (or an inline structural type, {x: i64, y: i64} as {x, ..}) works as the annotation.

Variant Patterns

Variant patterns match variants. Consider,

let v: [`Bare, `Arg(i64), `MoreArg(string, i64)] = `Bare;
v <- `Arg(42);
v <- `MoreArg("hello world", 42);
select v {
  `Bare => "it's bare, no argument",
  `Arg(i) => "it has an arg [i]",
  x@ `MoreArg(s, n) => "it's big [x] with args \"[s]\" and [n]"
}

produces

$ graphix test.gx
"it's bare, no argument"
"it has an arg 42"
"it's big `MoreArg(\"hello world\", 42) with args \"hello world\" and 42"

Variant patterns enforce the same kinds of match case checking as all the other pattern types

let v: [`Bare, `Arg(i64), `MoreArg(string, i64)] = `Bare;
v <- `Arg(42);
v <- `MoreArg("hello world", 42);
select v {
  `Bare => "it's bare, no argument",
  `Arg(i) => "it has an arg [i]",
  x@ `MoreArg(s, n) => "it's big [x] with args \"[s]\" and [n]",
  `Wrong => "this won't compile"
}

yields

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    pattern `Wrong will never match [`Arg(i64), `MoreArg(string, i64)], unused match cases

Literals, Ignore

You can match literals as well as bind variables, as you may have noticed, and the special pattern _ means match anything and don’t bind it to a variable.

Missing Features

A significant missing feature from patterns vs other languages is support for multiple alternative patterns in one arm. I plan to add this at some point.

Select and Connect

Using select and connect together is one way to iterate in Graphix. Consider,

let a = [1, 2, 3, 4, 5];
let len = 0;
select a {
  [x, tl..] => {
    len <- len + 1;
    a <- tl
  },
  _ => len
}

produces

$ graphix test.gx
5

This is not normally how we would get the length of an array in Graphix, or even how we would do something with every element of an array (see array::map and array::fold), however it illustrates the power of select and connect together.