Type Declarations
Type Synonyms
type T a b = [a] b
A type
declaration creates a synonym for a
pre-existing type expression, which may optionally have
arguments. A type synonym is transparently unfolded at
use sites and is treated as though the user had instead
written the body of the type synonym in line.
Type synonyms may mention other synonyms, but it is not
allowed to create a recursive collection of type synonyms.
Newtypes
newtype NewT a b = { seq : [a]b }
A newtype
declaration declares a new named type which is defined by
a record body. Unlike type synonyms, each named newtype
is treated
as a distinct type by the type checker, even if they have the same
bodies. Moreover, types created by a newtype
declaration will not be
members of any typeclasses, even if the record defining their body
would be. For the purposes of typechecking, two newtypes are
considered equal only if all their arguments are equal, even if the
arguments do not appear in the body of the newtype, or are otherwise
irrelevant. Just like type synonyms, newtypes are not allowed to form
recursive groups.
Every newtype
declaration brings into scope a new function with the
same name as the type which can be used to create values of the
newtype.
x : NewT 3 Integer
x = NewT { seq = [1,2,3] }
Just as with records, field projections can be used directly on values of newtypes to extract the values in the body of the type.
> sum x.seq
6
Enums
enum Maybe a = Nothing | Just a
An enum
declaration introduces a new named type, which is defined by a
collection of constructors. enum
declarations correspond to the notion of
algebraic data types, which are commonly found in other programming
languages. Each named enum
type is treated like a separate type, even if it
has the exact same constructors as another enum
type—in this way enum
is similar to newtype
and unlike type
synonyms.
Constructors. The only way to create a value of an enum
type is to
use one of its constructors. When used in an expression, the constructors
behave like an ordinary function, which has one parameter for each field of the
constructor. For example, the constructor Just
has a type like this:
Just: {a} a -> Maybe a
Constructors may have 0 or multiple fields, and values created with different constructors are always distinct.
Case Expressions. The only way to examine a value of an enum
type is
with a case
expression, which are similar to if
expressions:
case e of
Nothing -> 0
Just a -> a + 1
In this example, e
is an expression of type Maybe
:
if it was created with the
Nothing
constructor, then we’ll use the first branch of thecase
expression, and the result of the whole expression would be 0;if,
e
was created by applying theJust
constructor to some value (e.g,Just 2
), then we’ll use the second branch of thecase
expression, and the variablea
will be bound to the value of the field (e.g.,2
), and the whole expression will evaluate toa + 1
(e.g.,3
).
It is also possible to use just a variable (or _
) in a case expression
to define a catch-all clause—if a value does not match any of the previous
cases, then this branch will be used:
isNothing x =
case x of
Nothing -> True
_ -> False
``Option`` and ``Result``. Currently, Cryptol defines two enum
declarations in the Cryptol standard library: Option
and Result
:
enum Option a = None | Some a
enum Result t e = Ok t | Err e
The Option a
type represents an optional value, which can either be a value
of type a
(Some
) or no value at all None
. A value of type Result
t e
can either be a successful value of type t
(Ok
) or an error value
of type e
(Err
).
Option
and Result
values are commonly used to model the return type of
partial functions, i.e., functions that are not defined for all inputs. For
instance, if a function f
is not defined on the input 42
, then one
could model this with Option
:
f : [8] -> Option [8]
f x =
if x == 42
then None
else Some (x+1)
One could also model this with Result
:
f : [8] -> Result [8] (String [8])
f x =
if x == 42
then Err "`f 42` not defined"
else Ok (x+1)
With either result type, one can gracefully recover from f 42
erroring by
matching on None
or Err
in a case
expression.
Upper Case Restriction.
The names of the constructors in an enum
declarations
need to start with an upper-case letter. This restriction makes it possible
to distinguish between constructors and variable
bindings in case
patterns (e.g., between Just
and a
in the
previous example).
Non Recursive. The fields in a constructor may be of any value type, as long as this type does not depend on the type to which the constructor belongs. This means that we do not support defining recursive types, such as linked lists.
No Nested Constructor Patterns. For simplicity, only non-constructor
patterns may be used in the fields of a constructor pattern. For example,
Just (a,b)
and Just (a # b)
are OK, however, Just (Just a)
will be rejected. This is a restriction that we may lift in the future.
No Overlapping Patterns. For simplicity, all patterns in a
case
expression must be disjoint. In particular, this means that:
No two patterns in a
case
expression can match the same constructor. This means that Cryptol will reject the following example:isNothing x = case x of Nothing -> True Nothing -> FalseIf a
case
expression uses a catch-all clause, then that clause must occur last in the expression. It is an error to match on additional patterns after the catch-all clause. For instance, Cryptol will reject the following example:isNothing x = case x of Just _ -> False _ -> True Nothing -> False
Patterns Must Be Exhaustive. The patterns in a case
expression must
cover all constructors in the enum
type being matched on. For example,
Cryptol will reject the following example, as it does not cover the Just
constructor:
isNothing x =
case x of
Nothing -> True
The Matched Expression Must Have a Known Enum Type. Cryptol will reject
the following definition of f
, where f
lacks a type signature:
f x =
case x of
_ -> ()
This is because it is not clear what the type of x
(the expression being
matched) should be. The only pattern is a catch-all case, which does not reveal
anything about the type of x
. It would be incorrect to give f
this type:
f : {a} a -> ()
This is because f
is not really polymorphic in its argument type, as the
only values that can be matched in a case
expression are those whose type
was declared as an enum
. As such, Cryptol rejects this example.
Cryptol will also reject this definition, where the type of the value
being matched is not an enum
type:
g : Integer -> ()
g x =
case x of
_ -> ()