Note nº1: Emulating exact usage in Idris2¶
Idris2 is a programming language featuring Quantitative Type Theory. In its current implementation Idris2 uses the semiring \(\{0, 1, \omega\}\) which represent usage of a variable. Here, 0
indicates erasure 1
indicates linearity and ω
represents unrestricted usage.
Here is an example:
map : (f : (1 v : a) -> b) -> (1 ls : LList a) -> LList b
map f [] = []
map f (x :: xs) = f x :: map f xs
This is the map function defined on linear lists, a linear list is just like a list, except all its constructors are linear, which means we must consume each list exactly once.
As you can see, the function f
is also linear, it takes a v : a
that can be used only once and return a value of type b
. Crucially, you will notice that f
itself does not have a quantity associated with it, that means it's unrestricted, it can be used however meany times we want (including 0 times).
Now let's do the same with vectors:
map : (f : (1 v : a) -> b) -> (1 vs : LVect n a) -> LVect n b
map f [] = []
map f (x :: xs) = f x :: map f xs
In terms of implementation, this is what we expected. But notice the type signature: It is unchanged, even if we know how many times f
will be used. It will be used exactly n
times.
This is because Idris2's semiring does not allow us to express exact usage beyond 0
and 1
use. However we can trick it:
map : (0 f : (1 v : a) -> b) -> {auto 1 copies : Copies n f} -> (1 vs : LVect n a) -> LVect n b
map f [] {copies = Done} = []
map f (x :: xs) {copies = (More f cs)} = f x :: map f xs {copies=cs}
In this program f
has 0
uses, however it is accompanied with copies
which holds n
copies of f
itself. Since copies
has usage 1
it must be entirely consumed, forcing us to make use of every since copy that's available to us. In the implementation we pattern-match on copies
in order to extract the relevant (pun intended) copy of f
and use it on the values of the vectors to create a new vector. The auto
keyword is just there so that we don't have to write down the copies manually every time we want to use map
.
The only mystery remaining is to explain how Copies
is implemented. And it's fortunately fairly simple: It's a linear vector that can only keep one single element.
data Copies : Nat -> (0 v : a) -> Type where
Done : Copies 0 v
More : (1 v : a) -> (1 _ : Copies n v) -> Copies (S n) v
This way if we have a value Copies 3 "hello"
we get three copies of the value "hello"
. You will notice in the type of Copies
that the value itself must have 0
uses, that's because this only occurs in the type the value must be sorted in the More
constructor rather than be pulled from the type.
Just to check it works we can write a simple test:
testMap : Vectors.map LSucc [0,1,2,3] === [1,2,3,4]
testMap = Refl
Where LSucc
is the successor function for linear Nat
.
Thanks to Copies
we can now write programs that indicate exactly how many times a variable is used, even if the usage values we have are only 0
1
and ω
.