DocsLanguage

Types and patterns.

Model data with lists, records, and alternatives, then inspect it with patterns.

Types describe which values a function accepts and what it gives back. Fango infers many of them for you, but declaring a type is how you give your own data a name and a structure. This page starts with the built-in shapes, then moves on to types you define.

Lists.

import List

main() =
    numbers = [1, 2, 3]
    print (List.length numbers)
    print [0 | numbers]

This prints 3 and [0, 1, 2, 3]. All the elements of a list have the same type, so this one is a List Int. Lists are immutable. [head | tail] puts an element on the front, and the new list shares the existing tail instead of copying it.

The empty list is []. Use list syntax rather than the underlying Nil and Cons constructors.

Tuples.

A pair groups two values that may have different types: (42, "answer") has the type (Int, String). A triple holds three. The easiest way to get the parts back out is pattern matching:

main() =
    (number, label) = (42, "answer")
    print label
    print number

When the fields deserve names, or the data stands for something in your domain, use a record instead.

Records.

type Point = { x : Int, y : Int } deriving (Show)

moveRight : Point -> Point
moveRight point = { point | x = point.x + 1 }

main() =
    origin = Point { x = 0, y = 0 }
    print (moveRight origin)
    print origin

The first line of output is Point { x = 1, y = 0 } and the second is Point { x = 0, y = 0 }. Updating a record builds a new value and leaves the original as it was.

Records are nominal: the declared type decides their identity, so two declarations with identical fields are still different types. When you construct one, you supply every field exactly once, in any order.

Inferred records.

You can leave out the record name when the expected type makes it clear:

type Point = { x : Int, y : Int }

origin : Point
origin = { x = 0, y = 0 }

main() = print origin.x

Here the annotation tells Fango to build a Point. Field names alone never pick a record type, even when only one declaration matches. If the context doesn’t say enough, add a type annotation or write the record’s name.

Union types.

A union describes a value that can be one of several alternatives:

type Status = Waiting | Finished Int deriving (Show)

message : Status -> String
message status =
    case status of
        Waiting -> "still working"
        Finished count -> "processed #{count}"

main() = print (message (Finished 3))

This prints processed 3. Waiting carries nothing, while Finished carries an integer. These capitalized names are constructors. A type declaration defines a new nominal type, not an alias for an existing one.

Types can take parameters, which lets one definition describe a reusable shape. The prelude’s Maybe a is either Nothing or Just a, and it’s what you use when a value might be absent. Result error value tells an error apart from a successful value; you import it from Result.

Hidden state types.

A type variable that appears in a constructor’s fields, but not among the type’s parameters, is hidden. Each value picks its own type for it, and code using the value never sees which one:

type Counter = { count : state, step : state -> state, show : state -> String }

advance : Counter -> Counter
advance (Counter { count = current, step = next, show = render }) =
    Counter { count = next current, step = next, show = render }

describe : Counter -> String
describe (Counter { count = current, show = render }) = render current

numbers = Counter { count = 0, step = { n -> n + 1 }, show = { n -> "#{n}" } }
letters = Counter { count = "a", step = { text -> text ++ "a" }, show = { text -> text } }

main() = print (List.map { counter -> describe (advance counter) } [numbers, letters])
["1", "aa"]

numbers keeps an Int and letters a String, yet both are just a Counter, so they fit in one list. Inside a pattern, current, next, and render share one unknown state type: you can combine them with each other, or package them into a new constructor as advance does, but you can’t let current escape. Returning it from describe is a HIDDEN TYPE ESCAPES error.

Unpack related fields with a single pattern. Projecting a field that mentions a hidden type, as in counter.count, is rejected, though fields without one can still be read and updated. Only ordinary types can be hidden, not effect rows, and a type with hidden fields can’t use deriving.

Pattern matching.

A case picks the first branch that matches. Every branch must return the same type, and together the patterns must cover every possible value. Fango rejects missing cases and redundant branches, so you can’t forget one by accident.

firstOr : a -> List a -> a
firstOr fallback values =
    case values of
        [] -> fallback
        [first | _] -> first

main() = print (firstOr 0 [42, 7])

This prints 42. Patterns can look at constructors, list heads and tails, tuples, literals, and record fields. _ skips a part you don’t need, and a pattern can bind a name that the rest of its branch uses.

Record patterns and visibility.

A record pattern only needs to name the fields it cares about, as in Point { x = horizontal }, and the rest are ignored. The importing module must be able to see the record’s schema. If a module exports only a type name, construction and fields stay private; exporting Point(..) makes the schema public.

Destructuring bindings.

A binding such as (number, label) = pair takes a value apart in one step. These binding patterns must be exhaustive, so for a value with several alternatives, use case rather than assuming it’s one particular constructor. Inside a block, a binding may also bind no names at all: _ = expr runs expr and discards its result. A top-level destructuring binding must bind at least one name.

Deriving.

deriving (Eq, Ord, Show) asks the bundled derivers to write comparison and representation methods for you. Every field has to support the class you ask for. Deriving Show makes a value printable through the usual display fallback. Each class is separate, so deriving Ord doesn’t also derive Eq.

Next, Modules and imports shows how to choose which parts of your data model other modules can use.