blog/content/posts/2023-03-26-inductive-types.md

155 lines
4.9 KiB
Markdown
Raw Normal View History

2023-03-29 20:03:10 +00:00
+++
title = "Inductive Types"
slug = "inductive-types"
date = 2023-03-26
tags = ["type-theory"]
math = true
draft = true
+++
There's a feature common to many functional languages, the ability to have
algebraic data types. It might look something like this (OCaml syntax):
```ocaml
type bool =
| True
| False
```
For those who are unfamiliar with the syntax, I'm defining a type called `bool`
that has two _constructors_, or ways to create this type. The constructors are
`True` and `False`. This means:
1. Any time I use the value `True`, it's understood to have type `bool`.
2. Any time I use the value `False`, it's understood to have type `bool`.
3. In addition, there are _no_ other ways to create values of `bool` other than combining `True` and `False` constructors.
---
Many languages have this feature, under different names. Tagged unions, variant
types, enumerations, but they all reflect a basic idea: a type with a limited
set of variants.
Now, in type theory, one of the interesting things to know about a type is its
_cardinality_. For example, the type `Boolean` is defined to have cardinality 2.
That's because there's only one constructor, so if at any point you have some
unknown value of type `Boolean`, you know it can only take one of two values.
<details>
<summary>Note about Booleans</summary>
There's actually nothing special about boolean itself. I could just as easily
define a new type, like this:
```ocaml
type WeirdType =
| Foo
| Bar
```
Because this type can only have two values, it's _semantically_ equivalent to
the `Boolean` type. I could use it anywhere I would typically use `Boolean`.
I would have to define my own operators such as AND and OR separately, but
those aren't properties of the `Boolean` type itself, they are properties of
the Boolean algebra, which has several [algebraic properties][1] such as
associativity, commutativity, distributivity, and several others. Think of it
as a sort of _interface_, where if you can implement that interface, your type
qualifies as a Boolean algebra!
[1]: https://en.wikipedia.org/wiki/Boolean_algebra_(structure)#Definition
</details>
You can make any _finite_ type like this: just create an algebraic data type
with unit constructors, and the result is a type with a finite cardinality. If I
wanted to make a unit type for example:
```ocaml
type unit =
| Unit
```
There's only one way to ever construct something of this type, so the
cardinality of this type would be 1.
## Doing Things with Types
Creating types and making values of those data types is just the first part
though. It would be completely uninteresting if all we could do is create types.
So, the way we typically use these types is through _pattern matching_ (also
called structural matching in some languages).
Let's see an example. Suppose I have a type with three values, defined like
this:
```ocaml
type direction =
| Left
| Middle
| Right
```
If I was given a value with type `direction`, but I wanted to do different
things depending on exactly which direction it was, I could use _pattern
matching_ like this:
```ocaml
let do_something_with (d : direction) =
match d with
| Left -> do_this_if_left
| Middle -> do_this_if_middle
| Right -> do_this_if_right
```
This gives me a way to discriminate between the different variants of
`direction`.
> Most languages have a built-in construct for discriminating between values of
> the `Boolean` type, called if-else. What would if-else look like if you wrote
> it as a function in this pattern-matching form?
## The Algebra of Types
Finite-cardinality types like the ones we looked at just now are nice, but
they're not super interesting. If you had a programming language with nothing
but those, it would be very painful to write in! This is where _type
constructors_ come in.
When I say type constructor, I mean a type that can take types and build other
types out of them. There's several ways this can be done, but the one I want to
discuss today is called _inductive_ types.
> If you don't know what induction is, the [Wikipedia article][2] on it is a
> great place to start!
>
> [2]: https://en.wikipedia.org/wiki/Mathematical_induction
The general idea is that we can build types using either base cases (variants
that don't contain themselves as a type), or inductive cases (variants that _do_
contain themselves as a type).
You can see an example of this here:
```ocaml
type nat =
| Suc of nat
| Zero
```
These are the natural numbers, which are defined inductively. Each number is
just represented by a data type that wraps 0 that number of times. So 3 would be
`Suc (Suc (Suc Zero))`.
This data type is _inductive_ because the `Suc` case can contain arbitrarily
many `nat`s inside of it. This also means that if we want to talk about writing
any functions on `nat`, we just have to supply 2 cases instead of an infinite
number of cases:
```ocaml
let is_even = fun (x : nat) ->
match x with
| Suc n -> not (is_even n)
| Zero -> true
```