8.12

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:

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.

annot.macro 'AscendingIntList':

  'converting(fun (ints :: List.of(Int)) :: List:

                ints.sort())'

> [3, 1, 2] :: AscendingIntList

[1, 2, 3]

> fun descending(ints :: AscendingIntList):

    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

> [[1, 0], [4, 3, 2]] :: List.of(AscendingIntList)

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

  'converting(fun (s :: Bytes):

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

  'converting(fun (s :: Bytes):

                try:

                  Bytes.utf8_string(s)

                  ~catch _: #false)'

> #"\316\273" :: MaybeUTF8BytesAsString

"λ"

> #"\316" :: MaybeUTF8BytesAsString

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