1.3 Classes and 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
Comparing Posn.x to a function that uses .x on its argument, the difference is that Posn.x works only on Posn instances. That constraint makes field access via Posn.x 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, but that assertion is not checked when the argument is provided. In effect, the annotation simply selects a class-specific field accessor for .x. If flip is called with 0, then a run-time error will occur at the point that p.y attempts to access the y field of a Posn instance:
> flip(0)
Posn.y: contract violation
expected: Posn
given: 0
The :: binding operator is another way to annotate a variable. Unlike :~, :: installs a run-time check that a value supplied for the variable satisfies its annotation. The following variant of the flip function will report an error if its argument is not a Posn instance, and the error is from flip instead of delayed to the access of y:
> flip(0)
flip: argument does not satisfy annotation
argument: 0
annotation: Posn
A run-time check implied by :: can be expensive, depending on the annotation and context. In the case of flip, this check is unlikely to matter, but if a programmer uses :: everywhere to try to get maximum checking and maximum guarantees, it’s easy to create expensive function boundaries. Rhombus programmers are encouraged to use :~ when the goal is to hint for better performance, and use :: only where a defensive check is needed, such as for the arguments of an exported function.
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. 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
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