8.12

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.

def origin = Posn(0, 0)

> 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:

fun flip(p :~ Posn):

  Posn(p.y, p.x)

> 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:

fun flip(p :: Posn):

  Posn(p.y, p.x)

> 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:

def (flipped :~ Posn) = flip(Posn(1, 2))

> 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.

fun flip_ints(p :: Posn.of(Int, Int)):

  Posn(p.y, p.x)

> 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.

fun same_posn(p) :~ Posn:

  p

> same_posn(origin)

Posn(0, 0)

> same_posn(5)  // no error, since `:~` does not check

5

> same_posn(origin).x  // uses efficient field access

0

fun checked_same_posn(p) :: Posn:

  p

> 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.

#lang rhombus

 

fun get_after(): after

 

def accum = 0

let accum = accum+1

let accum = accum+1

accum  // prints 2

 

def after = 3

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.

fun omnivore(_): "yum"

fun omnivore2(_, _): "yum"

fun nomivore(_ :: Number): "yum"

> 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