5.5 Annotations as Converters
See Annotations versus Binding Patterns for an introduction to the interaction of annotations and bindings.
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. In that case, 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)'
annot.macro 'UTF8BytesAsString':
// matches only when `MaybeUTF8BytesAsString` produces a string
'converting(fun (str :: (MaybeUTF8BytesAsString && String)):
str)'
> #"\316\273" :: UTF8BytesAsString
"λ"
> #"\316" is_a UTF8BytesAsString
#false
> #"\316" :: UTF8BytesAsString
::: value does not satisfy annotation
value: #"\316"
annotation: UTF8BytesAsString
An annotation macro can create a convert annotation directly using annot_meta.pack_converter. When a macro parses annotations, it can use annot_meta.unpack_converter to handle all forms of annotations, since predicate annotations can be automatically generalized to converter form. A converter annotation will not unpack with annot_meta.unpack_predicate. Use annot_meta.is_predicate and annot_meta.is_converter to detect annotation shapes and specialize transformations.