7.4 Annotations as Converters
Unless otherwise specified, an annotation is a predicate annotation. For example, String and ReadableString are predicate annotations. When a predicate annotation is applied to a value with the :: expression operator, the result of the expression is the operator’s left-hand argument (or an exception is thrown). Similarly, using the :: binding operator with a predicate annotation has no effect on the binding other than checking whether a corresponding value satisfies the annotation’s predicate.
A converter annotation produces a result when applied to a value that is potentially different than the value. For example, ReadableString.to_string is a converter annotation that converts a mutable string to an immutable string. When a converter annotation is applied to a value with the :: expression operator, the result of the expression can be a different value that is derived from the operator’s left-hand argument. Similarly, using the :: binding operator with a converter annotation can change the incoming value that is matched against the pattern on the left-hand side of the operator.
A converting annotation cannot be used with :~, which skips the predicate associated with a predicate annotation, because conversion is not optional. Annotation operators and constructors generally accept both predicate and converter annotations, and the result is typically a predicate annotation if all given annotations are also predicate annotations.
The converting annotation constructor creates a new converter annotation given three pieces:
a binding pattern that is matched to an incoming value;
a body that can refer to variable bound in the pattern; and
an optional annotation for the result of conversion, which can be checked and can supply static information about the converted result.
Since these are the same pieces that a single-argument fun form would have, the converting constructor expects a fun “argument,” but one that is constrained to have a single argument binding without a keyword.
For example, the following AscendingIntList annotation matches any list of integers, but converts it to ensure that the integers are sorted.
> [3, 1, 2] :: AscendingIntList
[1, 2, 3]
ints.reverse()
> descending([1, 4, 0, 3, 2])
[4, 3, 2, 1, 0]
> [3, 1, 2] :~ AscendingIntList
:~: converter annotation not allowed in a non-checked position
[[0, 1], [2, 3, 4]]
When a converting annotation is used in a position that depends only on whether it matches, such as with is_a, then the converting body is not used.When used with is_a, the binding pattern is also used in match-only mode, so its “committer” and “binder” steps (as described in Binding Low-Level Protocol) are not used. When a further annotation wraps a converting annotation, however, the conversion must be computed to apply a predicate (even the Any predicate) or further conversion. The nested-annotation strategy is used in the following example for UTF8BytesAsString, where is useful because checking whether a byte string is a UTF-8 encoding might as well decode it. Annotation constructors like List.of similarly convert eagerly when given a converting annotation for elements, rather than checking and converting separately.
annot.macro 'UTF8BytesAsString_oops':
Bytes.utf8_string(s))'
> #"\316\273" :: UTF8BytesAsString_oops
"λ"
> #"\316" is_a UTF8BytesAsString_oops
#true
> #"\316" :: UTF8BytesAsString_oops
Bytes.utf8_string: byte string is not a well-formed UTF-8 encoding
byte string: Bytes.copy(#"\316")
annot.macro 'MaybeUTF8BytesAsString':
try:
~catch _: #false)'