2.3 Classes and Binding Patterns
In the same way that def and fun define variables and functions, class defines a new class. By convention, class names start with a capital letter.
class Posn(x, y)
A class name can be used like a function to construct an instance of the class. An instance expression followed by . and a field name extracts the field value from the instance.
> origin
Posn(0, 0)
> origin.x
0
A class name followed by . and a field name gets an accessor function to extract the field value from an instance of the class:
> Posn.x(origin)
0
The Posn.x to a function works only on Posn instances. In contrast, the .x in origin.x accesses an x field from any kind of object as the value of origin. The constraint on Posn.x make it more efficient than a generic lookup of a field with .x.
An annotation associated with a binding or expression can make field access with .x the same as using a class-specific accessor. Annotations are particularly encouraged for a function argument that is a class instance, and the annotation is written after the argument name with :: and the class name:
> flip(Posn(1, 2))
Posn(2, 1)
Using :: makes an assertion about values that are provided as arguments, and that assertion is checked when the argument is provided.
> flip(0)
flip: argument does not satisfy annotation
argument: 0
annotation: Posn
The :~ binding operator is another way to annotate a variable. Unlike ::, :~ does not imply an immediate run-time check. The following variant of the flip function accepts any value as its argument, and an error for a non-Posn argument is delayed to the access of y:
> flip(0)
Posn.y: contract violation
expected: Posn
given: 0
In effect, the :~ annotation here just selects a class-specific field accessor for .y and .x, the same as using Posn.y and Posn.x.
Normally, :: is preferred to :~ with a
class annotation, because the implied run-time check is inexpensive—
The use of :: or :~ as above is not specific to fun. The :: and :~ binding operators work in any binding position, including the one for def:
> flipped.x
2
The class Posn(x, y) definition does not place any constraints on its x and y fields, so using Posn as a annotation similarly does not imply any annotations on the field results. Instead of using just Posn as a annotation, however, you can use Posn.of followed by parentheses containing annotations for the x and y fields. More generally, a class definition binds the name so that .of accesses an annotation constructor.
> flip_ints(Posn(1, 2))
Posn(2, 1)
> flip_ints(Posn("a", 2))
flip_ints: argument does not satisfy annotation
argument: Posn("a", 2)
annotation: Posn.of(Int, Int)
Finally, a class name like Posn can also work in binding positions as a pattern-matching form. Here’s a implementation of flip that uses pattern matching for its argument:
fun flip(Posn(x, y)):
Posn(y, x)
> flip(0)
flip: argument does not satisfy annotation
argument: 0
annotation: matching(Posn(_, _))
> flip(Posn(1, 2))
Posn(2, 1)
As a function-argument pattern, Posn(x, y) both requires the argument to be a Posn instance and binds the identifiers x and y to the values of the instance’s fields. There’s no need to skip the check that the argument is a Posn, because the check is anyway part of extracting x and y fields.
As you would expect, the fields in a Posn binding pattern are themselves patterns. Here’s a function that works only on the origin:
fun flip_origin(Posn(0, 0)):
origin
> flip_origin(Posn(1, 2))
flip_origin: argument does not satisfy annotation
argument: Posn(1, 2)
annotation: matching(Posn(0, 0))
> flip_origin(origin)
Posn(0, 0)
Finally, a function can have a result annotation, which is written with :: or :~ after the parentheses for the function’s argument. With a :: result annotation, every return value from the function is checked against the annotation. That kind of checking is sometimes worthwhile, but :~ is more often appropriate to communicate the kind of result that a function definitely returns. Beware that a function’s body does not count as being tail position when the function is declared with a :: result annotation.
> same_posn(origin)
Posn(0, 0)
> same_posn(5)
5
> same_posn(origin).x
0
> checked_same_posn(origin)
Posn(0, 0)
> checked_same_posn(5)
checked_same_posn: result does not satisfy annotation
result: 5
annotation: Posn
The let form is like def, but it makes bindings available only after the definition, and it shadows any binding before, which is useful for binding a sequence of results to the same name. The let form does not change the binding region of other definitions, so a def after let binds a name that is visible before the let form.
fun get_after(): after
accum // prints 2
get_after() // prints 3
Normally, let is used for local definitions, while def is used for module-level definitions. Using let for module-level definitions can constrain exporting and hide definitions from a REPL.
The identifier _ is similar to Posn and :~ in the sense that it’s a binding operator. As a binding, _ matches any value and binds no variables. Use it as an argument name or subpattern form when you don’t need the corresponding argument or value, but _ nested in a binding pattern like :: can still constrain allowed values.
> omnivore(1)
"yum"
> omnivore("apple")
"yum"
> omnivore2("a", 1)
"yum"
> nomivore(1)
"yum"
> nomivore("a")
nomivore: argument does not satisfy annotation
argument: "a"
annotation: Number