Types
Type annotations
Type annotations describe the type of a function or value.
// Type: |---------------------|
function f(x: Int, y: Int) -> Int = x + y
Local bindings can also be annotated.
let a: Int = 3;
let (b, c): (Int, Int) = (100, 200);
let (d, e): (_, Int) = (300, 400) // Inference for d, specify type for e.
Declaring data types
Type declarations are only allowed at the module level.
All types in Fuyu are algebraic data types, and more specifically, sum types. A sum type is one that can take on exactly one value from a set of possible values, which are created using value constructors. The value constructors live at the top level of the module in which they are declared.
type Size
case Small
case Medium
case Large
function default_size() -> Size = Medium
A value of type Size can take on exactly one of the values Small, Medium, or Large,
which are value constructors of the type.
Value constructors can also have positional and named data associated with them to create product types.
type Order
case Tea(Size, String)
case Coffee(Size, cream: Bool, sugar: Bool)
function black_coffee(size: Size) -> Order = Coffee(size, False, sugar: False)
Named fields in data constructors follow nearly the same rules as named function arguments, with the only difference being that named fields in data constructors cannot specify different external names. This follows from the fact that there is nothing to rename, since there is no function mody associated with a data constructor.
Accessors
Accessors are used to simplify access to fields of a type.
- The
#0,#1, etc., accessors are used to access the values of the data constructor by position. - The
#field_nameaccessors are used to access the values of the data constructors by their names (if given).
type Person
case Person(String, age: Int, email: String)
Given a value Person("Calliope", age: 33, email: "calliope@example.com") bound to person,
the following are true:
person#nameis"Calliope".person#1andperson#ageare33.person#2andperson#emailare"calliope@example.com".
If there are multiple data constructors for a type, then the following rules apply:
- If the compiler can determine what data constructor was used for the value,
then both
#0and#field_nameaccessors work. - If all data constructors have the same type in the field position specified by a
#0style accessor, then the accessor works. - If all data constructors have the same type in the field name specified by a
#field_namestyle accessor, then the accessor works.
Record updates
The .. can be used to match extra fields in a pattern and to spread the existing fields for compact updates.
An existing value can serve as a reference for creating a new struct by listing it as the last entry,
prefixed with ...
The referenced value provides fallback values for any fields that aren't explicitly specified.
No more than one fallback may be provided.
function birthday(person: Person) -> Person =
let Person(age:, ..) = person; // Ignore extra fields in a pattern.
Person(age: age + 1, ..person) // Use `person` for the remaining fields.
The fields can be filled either positionally or by name (with the same rule as named function arguments), and the reference value fills in the rest.
If there are multiple data constructors for a type, then updates are only possible if the compiler can determine what data constructor was used for the reference value and it matches the data constructor used to create the new value, then the update works.
Type parameters
Types may be parameterized, and type parameters are listed after the type name. Generic type parameters begin with a lowercase letter, while concreate types begin with an uppercase letter.
type Either(a, b)
case This(a)
case That(b)
function swap(either: Either(a, b)) -> Either(b, a) = match either {
case This(x) => That(x)
case That(x) => This(x)
Recursive types
Types may store values that are of their own type.
For example, the Nodes of a Tree store the left subtree, the node value, and the right subtree,
while each Leaf simply stores a terminal value and ends recursion.
type Tree(a)
case Node(Tree(a), a, Tree(a))
case Leaf(a)
// 4
// / \
// Create this tree: 2 5
// / \
// 1 3
function tree() -> Tree(Int) = Node(Node(Leaf(1), 2, Leaf(3)), 4, Leaf(5))