Chapters

Lists, Generics, and Type Classes

Use the same algorithm with several types.

6.1 A list has two shapes

The standard List(A) consists of the empty list Nil or Cons(head, tail), which stores a first element and the remaining list.

ch06-list.taut
let numbers := Cons(1, Cons(2, Cons(3, Nil)))

fn list_sum(xs: List(i64)): i64 ! {Div} {
  match xs {
    Nil => 0
    Cons(first, rest) => first + list_sum(rest)
  }
}

perform show("sum", list_sum(numbers))

// Recurse over the list structure to add its elements.
Execution result
・ list_sum

sum = 6
sum = 6

Decomposing a List(i64) by its shape naturally covers both empty and nonempty lists. Recursing on the rest of Cons follows the structure of the list.

6.2 Type-independent functions

ch06-identity.taut
fn identity<A>(x: A): A {
  x
}

let n := identity(42)
let t := identity("Tauto")
perform show("n", n)
perform show("t", t)

// The same identity function preserves integers and text.
Execution result
✓ def identity : ∀ A: Type. (A) -> A

n = 42
t = "Tauto"
n = 42
t = "Tauto"

<A> declares a type parameter. The argument's type determines A, so you can omit it at a call. When applying a type, use parentheses, as in the previous chapter's Option(i64).

6.3 What where Ord(A) means

Comparing values of an arbitrary type requires a comparison operation. Tauto's Ord(A) type class includes not just a comparison function but also the laws an ordering must satisfy. A function declares its requirement with where.

Let's write insertion sort. Import std::Base for proof definitions, and explicitly import std::prelude::Ord for the ordering used by the program.

tauto init sorter
src/sort.taut
import std::Base
import std::prelude::Ord

pub fn insert<A>(x: A, l: List(A)): List(A) where Ord(A) {
  match l: List(A) {
    Nil => Cons(x, Nil)
    Cons(y, tail) => match Ord::compare(x, y): List(A) {
      Less => Cons(x, Cons(y, tail))
      Equal => Cons(x, Cons(y, tail))
      Greater => Cons(y, insert(x, tail))
    }
  }
}

pub fn sort<A>(l: List(A)): List(A) where Ord(A) {
  match l: List(A) {
    Nil => Nil
    Cons(x, rest) => insert(x, sort(rest))
  }
}

// Insert into an ordered list, then sort the tail recursively.
✓ Check passed
✓ def insert : ∀ A: Type. (Ord(A)) -> (A) -> (List(A)) -> List(A)
✓ def sort : ∀ A: Type. (Ord(A)) -> (List(A)) -> List(A)
src/main.taut
import "sort.taut"

perform show("ints", sort(Cons(3, Cons(1, Cons(2, Nil)))))
perform show("words", sort(Cons("pear", Cons("apple", Cons("fig", Nil)))))

// Reuse one ordering-based algorithm with numbers and words.
Execution result
ints = Cons(1, Cons(2, Cons(3, Nil)))
words = Cons("apple", Cons("fig", Cons("pear", Nil)))
tauto run --tote sorter
ints = Cons(1, Cons(2, Cons(3, Nil)))
words = Cons("apple", Cons("fig", Cons("pear", Nil)))

If the comparison is Less or Equal, insert at the front; for Greater, inspect the next element. The same sort works with numbers and strings. Ord::compare(x, y) is the comparison operation, and match handles Less, Equal, and Greater.

Because the class is defined so that its laws can be proved, you can state sorting properties independently of a particular numeric type (Chapter 18).

Exercise 6.1: Explain what identity has in common with a function that decomposes a list one layer at a time.