Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Complex Data, Part I: Lists and Option

import Lait
#lait

So far, we have only seen basic data types, such as Int and Bool, function types, and product types, such as Int * Int. We will now move on more complex data types.

Lists

For any type T, List<T> is the type of lists of values of type T. To create a list, we have the value Nil, which has type List<T> for any type T; and the function Cons, which has type (a -> (List<a> -> List<a>)); that is, it takes in a value of type a and a list of type List<a> and returns a list of type List<a>.

#check Nil>> List<a>
#check Cons>> a -> List<a> -> List<a>
#eval Cons 1 (Cons 2 (Cons 3 Nil)) >> Cons 1 (Cons 2 (Cons 3 Nil))

Using Lists

To use a list, we pattern match on it.

def sumList (xs : List<Int>) : Int  := 
  match xs with
  | Nil => 0
  | Cons h t => h + sumList t
  end

#eval sumList [1, 2, 3]>> 6

Every patern match must begin with match e with, where e is an expression; and must end with end. For lists, pattern matching must analyze two cases: the Nil case, and the Cons case. If we forget one of the two cases, Lait will throw an error:

def badSumList (xs : List<Int>) : Int  := 
  match xs with
  | Cons h t => h + badSumList t
  end

Abbreviations for Lists

Since lists are so common, we also have abbreviations. First, Nil and Cons are synonymous with [] and ::. This can be used both for creating lists and pattern matching on lists.

#eval 1 :: 2 :: 3 :: [] >> Cons 1 (Cons 2 (Cons 3 Nil))

def sumList2 xs := 
  match xs with
  | [] => 0
  | h :: t => h + sumList2 t
  end

In addition, when making a particular list, we can use the syntax [e1, e2, ..., en], where e1, e2, …, en are expressions of type t:

#eval [1, 2, 3] >> Cons 1 (Cons 2 (Cons 3 Nil))

Here, [1, 2, 3] is exactly equivalent to 1 :: 2 :: 3 :: [], which is in turn exactly equivalent to Cons(1, Cons(2, Cons(3, Nil))).

Common List Operations

Lait has a number of list operations built in to its standard library. We give a few examples here; more can be seen in the standard library reference.

List.append : List<a> -> List<a> -> List<a> takes two lists and returns a new list that joins them together:

#eval List.append [1, 2, 3] [4, 5, 6] >> Cons 1 (Cons 2 (Cons 3 (Cons 4 (Cons 5 (Cons 6 Nil)))))

Identifiers in Lait can have periods in the middle of them. We use this as a simple form of namespacing: List.append is the name we give to an operation on lists (as opposed to an append operation on, say, strings).

List.length : List<a> -> Int takes a list and returns its length:

#eval List.length [1, 2, 3]>> 3

List.member : List<a> -> a -> Bool takes a list and a value and returns true if the value is in the list, and false otherwise:

#eval List.member [1, 2, 3] 2>> true
#eval List.member [1, 2, 3] 4 >> false

Note that List.member only works for lists of types that can be compared for equality (e.g., Int), and will throw an error if the list contains functions.

List.filter : List<a> -> (a -> Bool) -> List<a> takes a list and a predicate and returns a new list that contains only the elements of the original list that satisfy the predicate:

#eval List.filter [1, 2, 3] (fun x => x >= 2) >> Cons 2 (Cons 3 Nil)

List.find : List<a> -> (a -> Bool) -> Option<a> takes a list and a predicate and returns the first element of the list that satisfies the predicate, or None if no element satisfies the predicate:

#eval List.find [1, 2, 3] (fun x => x >= 2) >> Some 2
#eval List.find [1, 2, 3] (fun x => x > 3)>> None

(Option types are discussed below.)

Options

Lists are one example of user-defined data type, which is built up out of constructors (e.g., Nil and Cons for lists). Another common example built into Lait are option types. Given a type t, a value of type Option<t> is either None or Some x, where x is a value of type t. Hence, an option type is used when a value may or may not be present.

#check None  >> Option<a>
#check Some 1>> Option<Int>

To use an option type, we pattern match on it, similar to lists. Below is a worked example:

def List.getFirst (xs : List<a>) : Option<a> :=
  match xs with 
  | [] => None
  | x :: _ => Some x
  end

def List.isFirstPositive (xs : List<Int>) : Bool :=
  match List.getFirst xs with 
  | None => false -- List is empty, so head is not positive
  | Some x => x > 0 -- Otherwise, return whether that first element is positive
  end

First, we create a function List.getFirst that gets the first element of a list, or None if the list is empty. Then, we create a function List.isFirstPositive that uses List.getFirst to get the first element if it exists; if it does, we return whether it is greater than zero.

Pattern Matching: No Nesting

Lait does not support nested pattern matching, where we match on multiple constructors at once. Thus, the following does not work:

def getSecondOrZero (xs : List<Int>) : Int :=
match xs with
 | Cons x (Cons y ys) => y
 | Cons x Nil => 0
 | Nil => 0
end

When we pattern match on a constructor (be it Cons or a user-defined one), we can only pattern match “one layer deep”. Hence, the two arguments to Cons in a pattern match must both be variables; not constructors themselves (such as Cons or Nil). So the above needs to instead be written:

def getSecondOrZero (xs : List<Int>) : Int :=
  match xs with
  | Cons x xs' => 
    match xs' with
    | Cons y zs => y
    | Nil => 0
    end
  | Nil => 0
  end