Data
Values and continuations meet in a cut. Four declarations name the shapes of that meeting.
data and form carry every field. A data value gives the fields. A
form is a continuation that wants the fields.
enum and menu carry one alternative. An enum value is the variant it
picked. A menu value answers the one item the continuation picks.
Each declaration has an anonymous spelling. A tuple is (A, B), a choice is
(A | B), a bundle is (A & B), and a joint is (A ; B).
// data carries every field. form wants every field.
// enum is one variant. menu answers one item.
data Pair {
left: i64,
right: i64,
}
form Total {
left: i64,
right: i64,
}
enum Colour {
Red,
Green,
}
menu Config {
retries: i64,
name: String,
}
func sum(p: Pair) -> i64 {
of p {
Pair { left, right } => <(left, right) | add,
}
}
func total(out: -i64) -> Total {
mu Total {
Total { left, right } => <(left, right) | add | out>,
}
}
func name(c: Colour) -> String {
of c {
Red => "red",
Green => "green",
}
}
func config() -> Config {
mu Config {
retries <= <3 | retries>,
name <= <"SLC" | name>,
}
}
proc main | (exit: i32) / {IO} {
let p = Pair { left: 2, right: 40 };
<p.left | println;
<p | sum | println;
<mu i64 { answer <= <Total { left: 2, right: 40 } | (<answer | total)> } | println;
<Colour::Green | name | println;
<config().retries | println;
<config().name | println;
<0 | exit>
}
2
42
42
green
3
SLC
data
A data declaration names a value that carries every field.
data Pair {
left: i64,
right: i64,
}
Pair is the name of the type. Each line inside the braces is one field: a
name, then the type of the value stored there. left stores an i64, and
so does right. The list order is the order later code writes.
Building a value
Pair { left: 2, right: 40 } is a Pair. The literal names every field,
each expression has that field's type, and the names follow the declaration.
The program binds it to p.
Leaving right out is refused:
`Pair` has fields `left`, `right`; the literal writes `left`
Writing right before left is refused:
`Pair` has fields `left`, `right`; the literal writes `right`, `left`
Reading one field
p.left is the left field, an i64. The program prints 2. p.right
is the other field. A name that the declaration does not list is refused:
`Pair` has no field `middle`
Taking the value apart
of p takes the whole value apart. A data value has one shape, so there
is one arm, and the arm binds every field:
of p {
Pair { left, right } => <(left, right) | add,
}
A bare field name binds a variable of that name. Pair { left: x, right }
binds the first field as x and the second as right. The fields in the
pattern follow the declaration. Reversing them is refused:
`Pair` has fields `left`, `right`; the pattern writes `right`, `left`
The sum of 2 and 40 is 42.
A parameter can open the value in the header. The pattern is that one parameter, and it matches every value of the type:
func skew((a, b): (i64, i64), c: i64) -> i64 {
<(a, c) | mul | x => (x, b) | sub
}
data Point { x: i64, y: i64 }
func norm(Point { x, y }: Point) -> i64 {
<(x, x) | mul | z => (z, <(y, y) | mul) | add
}
examples/basics/patterns.sl
runs these, and binds a tuple and a Point with let the same way.
The tuple
A tuple (2, 40) is this connective with the name left off. Its type is
(i64, i64). A longer tuple is (A, B, C). Nesting matters: (10, (20, 30)) has two components, and its .1 is the pair (20, 30). .0 reads a
component, counting from zero. The unit (,) is the product with no fields,
and (,) is its value. The name unit is that type as well.
enum
An enum declaration names a value that is one variant.
enum Colour {
Red,
Green,
}
enum Shape {
Circle(i64),
Rect(i64, i64),
}
Red and Green carry no payload. Colour::Green is a Colour.
Circle carries an i64, so Shape::Circle is a constructor, and
Shape::Circle(5) is a Shape. Rect carries two integers. Several
payload types are one tuple, so the constructor takes (i64, i64), and the
pattern Rect(w, h) binds both components.
One arm for each variant
of c has an arm for each variant. The arm's pattern is that variant. Each
variant appears once, and the order of the arms does not matter. Green
produces "green", which the program prints.
Leaving a variant out is refused:
non-exhaustive of: missing variant `Green` of enum `Colour`
// An enum, an exhaustive of, a wildcard, and an inclusive range.
enum Shape {
Circle(i64),
Rect(i64, i64),
}
func area(s: Shape) -> i64 {
of s {
Circle(r) => <(3, r) | mul | x => (x, r) | mul,
Rect(w, h) => <(w, h) | mul,
}
}
proc main | (exit: i32) / {IO} {
<Shape::Circle(5) | area | println;
<Shape::Rect(6, 7) | area | println;
let n = of 2 { 0 => "zero", 1 => "one", _ => "many" };
<n | println;
let band = of 4 { 1..=3 => "low", 4..=6 => "mid", _ => "high" };
<band | println;
<0 | exit>
}
75
42
many
mid
Shape::Circle(5) builds a circle. area matches it and multiplies. The
circle arm uses a chain binder, x =>, to name the product <(3, r) | mul
before multiplying by the radius again. The area of a circle of radius 5,
with 3 standing in for π, is 75. The rectangle is 6 * 7, which is 42.
A continuation of a Shape is area_of in Both arrows. A
Shape value arrives in each arm there, so the arm arrow is =>, and each
arm sends the area on with a cut. An arm that is a bare value is refused.
mu Colour { Red => 0, … } reports:
a `mu` arm is a command; this one has type +i64
Literals, wildcards, and ranges
A wildcard _ covers whatever the earlier arms left. In of 2 { 0 => "zero", 1 => "one", _ => "many" } the wildcard is the remaining integers.
An inclusive range is start..=end. It takes the numeric type of the
scrutinee. 4 falls in 4..=6 and the arm produces "mid". A literal arm
and a range belong in an of. A mu arm covers one shape of a declaration,
and a nested of is where a literal is distinguished.
Or-patterns and @
// Or-patterns and an @ binding.
enum Hue { Red, Blue, Green }
func name(h: Hue) -> String {
of h {
Red | Blue => "cool",
Green => "green",
}
}
proc main | (exit: i32) / {IO} {
<Hue::Red | name | println;
<Hue::Green | name | println;
let tagged = of (1, 2) { n @ (a, _) => <(n.0, a) | add };
<tagged | println;
<0 | exit>
}
cool
green
2
Red | Blue is one arm covering two variants. @ binds the whole value and
still takes it apart: n @ (a, _) names the pair n and its first component
a. <(n.0, a) | add is 1 + 1.
A payload that is itself a record can be opened in the same pattern,
At(Point { x, y }). A sum nested in a payload stays in that payload.
Match it with an of inside the arm.
Constructors in a pattern are paths: Hue::Red, or Red after cite Hue::*;. The type name stays a separate cite when the signature also writes
Hue. cite Hue::*; brings the variants, and cite Hue; brings the type.
The early cut cites list::List and list::List::*
that way.
The choice
A choice (i64 | String) is an enum with the name left off. ::0(v) and
::1(v) are the alternatives, counting from zero, and ::0(x) is the
pattern. A longer choice nests to the right, and ::2(v) counts along that
nesting. The unit (|) is the choice with no alternatives, and it has no
value. An of on one has no arm.
examples/basics/sums.sl
builds choices with ::0 and ::1 and takes them apart.
menu
A menu declaration names a value that answers one item. The continuation
that uses the menu picks the item.
menu Config {
retries: i64,
name: String,
}
retries: i64 is an item whose answer is an i64. name: String is an
item whose answer is a String. A Config value has an answer for each
item. Using the value runs one of them.
Building a menu
config returns a Config. mu Config builds it. Each arm is one item,
and each item appears once. A request arrives in the arm, so the arrow is
<=:
mu Config {
retries <= <3 | retries>,
name <= <"SLC" | name>,
}
retries <= <3 | retries> names the continuation carried by that request
retries, and sends 3 to it. The other spelling names the continuation
on its own:
retries: out <= <3 | out>
The type after : is what the continuation accepts. Here it accepts the
item's answer, an i64. The picked arm is the one that runs.
Leaving an item out is refused:
non-exhaustive `mu Config`: missing items name
Writing the same item twice is refused:
`mu Config` has duplicate arm for `retries`
Picking an item
config().retries picks retries. The answer is the i64 3, and the
program prints it. config().name picks name and prints SLC.
A request
.retries(a) is the request itself: the item, and the continuation a
that receives the i64. Its type is -Config.
of takes a named request apart, one arm per item:
// of takes a request apart. Each arm answers with a request.
menu Config {
retries: i64,
name: String,
}
func config() -> Config {
mu Config {
retries <= <3 | retries>,
name <= <"SLC" | name>,
}
}
func reroute(k: -Config) -> -Config {
of k {
.retries(out) <= .retries(out),
.name(out) <= .name(out),
}
}
proc main | (exit: i32) / {IO} {
<mu i64 { a <= <config() | (<.retries(a) | reroute)> } | println;
<0 | exit>
}
3
reroute has an arm for each item. .retries(out) binds the continuation
the request carries, and the arm answers with .retries(out) again. The cut
<config() | (<.retries(a) | reroute)> sends the request to the menu.
reroute answers with that same request, and a receives 3.
A field with arguments
A field may take arguments. The field is then a function of those arguments.
The type after : is the function's result, and in an arm the name after
: is still the continuation that receives that result.
// A menu field is a function. The call supplies the arguments.
menu Pair {
both(x: i64, y: i64): i64,
}
func numbers() -> Pair {
mu Pair {
both(x, y): out <= <(x, y) | add | out>,
}
}
proc main | (exit: i32) / {IO} {
<numbers().both(2, 3) | println;
<0 | exit>
}
5
both(x: i64, y: i64): i64 declares the item. A call supplies (x, y), and
the result is an i64. The arm binds the arguments and then the
continuation:
both(x, y): out <= <(x, y) | add | out>
x and y are the arguments. out receives the sum. numbers().both(2, 3)
calls the function, and the sum is 5. The same call written as a stage is
<(2, 3) | numbers().both. The name both alone is the function.
both(x, y) <= … names that continuation both.
The bundle
A bundle (k1 & k2) is a menu with the name left off. The continuation
that uses it picks one item by position. The unit (&) is the menu with no
items, and (&) is its value.
examples/basics/sums.sl
sends a choice to a bundle of two continuations. The alternative's position
picks the continuation.
form
A form declaration names a continuation that wants every field.
form Total {
left: i64,
right: i64,
}
func total(out: -i64) -> Total {
mu Total {
Total { left, right } => <(left, right) | add | out>,
}
}
left: i64 and right: i64 name what flows in. mu Total builds the
continuation. A record arrives, so there is one arm, the arrow is =>, and
the arm binds left and right together. The arm is a cut: it sends their
sum to out.
total is a -> function. Its result is the Total. The caller gives it
a continuation of the i64 and gets the form back.
The record that feeds the form is Total { left: 2, right: 40 }. The field
names follow the declaration, as they do for Pair. Reversing them is
refused:
`Total` has fields `left`, `right`; the literal writes `right`, `left`
In the program the cut is:
<mu i64 { answer <= <Total { left: 2, right: 40 } | (<answer | total)> } | println
Read it from the continuation outward. answer is the continuation of the
i64. <answer | total gives total that continuation, and the result is
a Total. The record is sent to that Total. The sum 42 reaches
answer, and println prints it.
A form takes every field at once. A parameter t: Total is the form, and
t.left is refused:
`Total` is a form, and a form takes every field at once: send it one, `<Total { … } | …>`. A single field cannot be taken out of it — the others would have to be invented
Several statements, and a second form
An arm with several statements uses braces. When those statements perform
IO, the form's declaration carries that row.
// A form wants every field. An arm that performs IO belongs to the form's row.
form Report / {IO} {
value: i64,
label: String,
}
func printer(out: -i64) -> Report {
mu Report {
Report { value, label } => {
<label | println;
<value | out>
},
}
}
func shouting(next: Report) -> Report {
mu Report {
Report { value, label } => <Report { value: value, label: <(label, "!") | add } | next>,
}
}
proc main | (exit: i32) / {IO} {
<mu i64 { a <= <Report { value: 42, label: "answer" } | (<a | printer)> } | println;
<mu i64 {
a <= <Report { value: 7, label: "relabelled" } | (<a | printer | shouting)>,
} | println;
<0 | exit>
}
answer
42
relabelled!
7
Report / {IO} allows the arm to perform IO. printer prints label
and sends value to out. The first cut feeds Report { value: 42, label: "answer" }. The label is printed, and 42 reaches println.
shouting is a Report built from another Report. Its arm rebuilds the
record with "!" added to the label and sends that record to next. In
<a | printer | shouting, next is the Report that printer returned,
so the label is printed with the extra "!" and the value 7 reaches
println.
examples/duality/form.sl
is the same pair of forms.
The joint
A joint is a form with the name left off. (-i64 ; -i64) is two
continuations of an i64, one for each component of a pair. (k1 ; k2)
builds that joint from the two continuations. Continuations
builds the same continuation in one arm, mu (i64, i64) { (a, b) => … },
where the pair arrives together. A joint has no pattern that splits it back
into k1 and k2. What is taken apart is the pair that feeds it.
// A joint sends the pair's first component to the first continuation.
proc main | (exit: i32) / {IO} {
<(6, 7) | (
fn(n: i64) -> (;) {
<n | println;
<0 | exit>
}
; fn(m: i64) -> (;) {
<m | println;
<1 | exit>
}
)>
}
6
(6, 7) is sent to the joint. Delivery starts with 6 and the first
continuation. That continuation prints 6 and cuts to exit with 0, and
the process status is 0.
The unit (;) is the joint with no fields. It is the type of a cut, and it
has no value.
Both arrows writes the <- version of total: a function
given a continuation of the sum, returning a continuation of a data value
or a tuple.