4.9 KiB
+++ 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):
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:
- Any time I use the value
True
, it's understood to have typebool
. - Any time I use the value
False
, it's understood to have typebool
. - In addition, there are no other ways to create values of
bool
other than combiningTrue
andFalse
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.
Note about Booleans
There's actually nothing special about boolean itself. I could just as easily define a new type, like this:
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 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!
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:
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:
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:
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 on it is a great place to start!
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:
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:
let is_even = fun (x : nat) ->
match x with
| Suc n -> not (is_even n)
| Zero -> true