Expressive Types

Martin Sulzmann

Overview

By expressive types we mean:

Next, we take a look at a series of Go examples. The complete program code can be found here.

Example: Physical units

Here is a standard school book exericse:

Below is an implementation in Go.

func perimeterRec(l int, h int) int {
    return l + l + h + h
}

func ex_rec() {
    length := 10 // measured in feet

    height := 5 // measured in meters

    p := perimeterRec(length, height)

    fmt.Printf("\np = %d", p)
}

Any issues?

Hence, we might get some bogus result but the program runs just fine and the bug (physical unit mismatch) goes undetected.

Expressive types to the rescue

Idea:

Here’s how we achieve this in Go.

Physical units aware integer variables

type dist[A any] struct {
    val int
}

func add[A any](x dist[A], y dist[A]) dist[A] {
    return dist[A]{x.val + y.val}
}

Here’s a recast of the above example where we assume the physical units feet and meters.

type meters struct{}
type feet struct{}


func perimeterRec_safe(l dist[meters], h dist[meters]) dist[meters] {
    return add(l, add(l, add(h, h)))
}

func ex_rec_safe() {
    length := dist[meters]{10}

    height := dist[meters]{5}

    p := perimeterRec_safe(length, height)

    fmt.Printf("\np = %d", p.val)
}

/*

func ex_rec_wont_compile() {
    length := dist[feet]{10}

        height := dist[meters]{5}

        p := perimeterRec_safe(length, height)

        fmt.Printf("\np = %d", p.val)
    }

*/

Extensions

As you have noticed, instead of 2 * (l + h) we use the (equivalent) expression l+l+h+h.

If we include multiplication, matters become more complicated.

It’s possible to represent some more complex phyiscal units in Go but things become more complicated.

Example: Extracting the head element of a slice

Consider the following program text.

func head(xs []int) int {
    return xs[0]
}

func ex_hd() {
    xs := []int{1, 2, 3}
    // The following variant yields panic.
    /// xs := []int{}
    x := head(xs)

    fmt.Printf("\n%d", x)

}

Here’s a variant where we catch the bug (extracting the head of an empty slice) at run-time.

func head_rt(xs []int) (int, bool) {
    if len(xs) == 0 {
        return 0, false
    }

    return xs[0], true
}

func ex_hd_rt() {
    xs := []int{1, 2, 3}

    x, b := head_rt(xs)
    if !b {
        fmt.Println("some panic, do something")
    }
    fmt.Printf("\n%d", x)

}

Challenge:

Expressive types to the rescue

Idea:

Encoding numbers at the level of types

type zero struct{}

type succ[T nat] struct {
    succ T
}

type nat interface {
    length() int
}

func (x zero) length() int {
    return 0
}

func (x succ[T]) length() int {
    return 1 + x.succ.length()
}

Length-indexed lists

type list[T any, L nat] struct {
    list []T
}

func head_list[T any, L nat](xs list[T, succ[L]]) T {
    return xs.list[0]
}

func push_back[T any, L nat](xs list[T, L], x T) list[T, succ[L]] {
    var ls []T
    ls = append(xs.list, x)
    return list[T, succ[L]]{ls}
}

func mk_list1[T any](x T) list[T, succ[zero]] {
    xs := []T{x}
    return list[T, succ[zero]]{xs}
}

func mk_list0[T any]() list[T, zero] {
    xs := []T{}
    return list[T, zero]{xs}
}

Here’s a recast of the above example where we make use of length-indexed lists.

func ex_hd_list() {
    xs := mk_list0[int]()
    ys := push_back(xs, 1)
    x := head_list(ys)

    fmt.Printf("\n%d", x)
}

Summary and some references

Expressive types are an old idea (they also appear under many names, e.g., phantom types, …)

It’s possible to encode expressive types in today’s programming languages (Go, Java, …)

The future:

Some references

Robert Harper, Type systems for programming languages

Benjamin Pierce’s book collection: