In which I Misunderstand Dependent Types

Most blog posts that deal with the more mathematical/category theory side of programming go over my head when I read them, only to become suddenly clear a few weeks down the line. I have not reached this such sudden clarity with this post, but I’m starting to see a glimmer.

In particular, I was reminded of this post on the Apple developer forums. It describes a way to write a generic vector type, with the length of the vector encoded into the type. Sounds a lot like length-indexed lists, no? I thought so.

Basically, the type was recursive. An empty vector has the type of EmptyVector, a vector of length 1 has the type of Vector<EmptyVector>, of length 2 is Vector<Vector<EmptyVector>>, and so on. Both the struct Vector and EmptyVector conformed to the protocol VectorType, which allowed for the polymorphism.

Inside the struct, the values were managed in a recursive-list style. Indexing looked kind of like this:

So on every next() call, self is replaced by the tail (xs), and the head (x) is returned. However, because a vector’s type is dependent on its length, you cannot replace self with its tail, because the tail is a different type. So for every index call, all the predecessors in the vector must be traversed.

In fact, what we’re looking for isn’t really a recursive list at all. It’s certainly elegant, and it has all that cool functional allure, but it’s not necessary. All we really care about is the length: we want to encode that as a type. So let’s do it! Here again, protocols are going to give us the required power.

So, what kind of thing is length? Well, it will be an integer greater than or equal to zero. A natural number, then:

public protocol Nat {}

We don’t want it to carry any values, because all we’re interested in should be resolved at compile-time. We’re going to have to use types inference like control flow for a bit, in order to get what we want. The first number to encode is 0:

public struct Zero : Nat {}

And then one, two, and so on. Except that’s silly. Obviously what we want is some more of that recursion. So a new protocol, then:

public protocol NonZero : Nat { typealias Pred : Nat }

So it has a typealias Pred: representing the number that comes before it. Pred can be any natural number. Let’s make a struct conform:

public struct Succ<P: Nat> : NonZero { public typealias Pred = P }

This works just as you’d expect: Succ is the successor to N. So, for instance, one two and three look like this:

It’s no good without an initialiser. Now, exposing one would be pretty useless: you could just create a ConstArray<Int, Succ>(): an array with a different length to the one its type states. A free function is the other option:

You’ll notice none of these functions are mutating: you can’t change the length without affecting the type, and we can’t change the type in Swift at all. So the usual append becomes appended, and so on.

Since arrays will have to be built this way, a custom operator is probably justified:

How about functions that remove items from an array? They won’t be able to mutate anything, they’ll have to return the item and the shorter array in a tuple, but that’s par for the course in some languages. Let’s try it:

So what are the types it should return? The count of the ConstArray should be one less than the count of self, obviously. Not all types that conform to Nat have a Pred typealias, though. Only the nonzeroes. Luckily, we have a protocol for that: NonZero. So here’s what our extension signature looks like:

public extension ConstArray where Count : NonZero {}

That is awesome. Just by the logic of the types themselves, we’ve found that we can only define remove operations on arrays with nonzero lengths! Here’s what they look like:

Well, first off, I don’t want to take credit for the vector implementation. What you see here is an adaption of Dave Abraham’s implementation, which is linked above.

Secondly, it’s really not intended to replace SIMD, or anything like it. It’s more of a proof-of-concept of one of the textbook patterns for dependent types. The vector in particular is a toy example. The ConstArray, though, is possibly useful.