Fin _
is one of those data types that makes you take a step back and question how much you really understand typed functional programming.
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
At first glance so a simple and similar to Nat
, yet it so much more. Like Nat
, this is a recursive type with a base case FZ
and a recursive case FS
. Also notice that unlike Nat
, this is a dependent type.
Fin _
requires a n : Nat
in order to construct a type. In the FZ
case that Nat
must be be S k
, that is to say it must be greater then zero. In the recursive case FS
, we provide a Fin k
and yield a Fin (S k)
, a Fin _
larger by one.
Edwin Brady describes Fin _
as the type of Finite Sets. That is not to say that Fin _
is a container, but rather that it represents the canonical set of unnamed elements.
λΠ> the (Fin 1) FZ
FZ
λΠ> :t the (Fin 1) FZ
the (Fin 1) FZ : Fin 1
Here we have FZ
as the zero element of the one element set. FZ
requires a S k
so this is the smallest possible finite set we can represent. We can represent the zeroth element of larger set by simply declaring it so:
λΠ> the (Fin 12) FZ
FZ
λΠ> :t the (Fin 12) FZ
the (Fin 12) FZ : Fin 12
We use FS
to construct subsequent elements of our finite set. For example, the second element of the two element set would be:
λΠ> the (Fin 2) (FS FZ)
FS FZ : Fin 2
λΠ> :t the (Fin 2) (FS FZ)
the (Fin 2) (FS FZ) : Fin 2
FZ
must be of type Fin (S k)
and FS
is Fin n -> Fin (S
n)
. Thus FS FZ
produces type Fin (S (S k))
and k
must be Z
in order to match our ascribed type Fin 2
.
Let's try to produce an element larger then the ascribed set:
λΠ> the (Fin 1) (FS FZ)
(interactive):1:14--1:19:When unifying Fin (S (S ?k)) and Fin 1
Mismatch between:
S ?k
and
0
at:
1 the (Fin 1) (FS FZ)
^^^^^
Here we tried to produce a second element of the one element set. Idris is telling us that the only way to construct FS FZ : Fin
1
would be to have FZ : Fin 0
. This would allow FS
to type check correctly as Fin 1
, however by the definition of Fin _
, FZ : Fin 0
is impossible as FZ
must have type S k
.
One natural use case for Fin _
is type safe indexing into Vect
:
index : Fin len -> Vect len elem -> elem
index FZ (x::xs) = x
index (FS k) (x::xs) = index k xs
Fin _
and Vect
are parameterized by len : Nat
. This means that we cannot construct Fin
values which are out of bounds for our Vect
. Out of bounds indices are literally unrepresentable. In my last blog post we took advantage of this property to create a Representable Functor
.
λΠ> index 2 (the (Vect 3 String) ["foo", "bar", "baz"])
"baz"
λΠ> index 3 (the (Vect 3 String) ["foo", "bar", "baz"])
(interactive):1:7--1:8:Can't find an implementation for IsJust (integerToFin 3 3) at:
1 index 3 (the (Vect 3 String) ["foo", "bar", "baz"])
^
The error messaging in Idris2 is a lot less clear then Idris1, but this is saying that there is not a valid way to convert a 3 : Nat
into a Fin 3
.
Another interesting use case for Fin _
, and the main topic of this blog post, is Modular Arithmetic where the Fin _
upper bound is the modulus.
The basic idea is that we perform addition using recursion similiar to Nat
arithmetic, however when we hit the upperbound we continue the operation from FZ
.
We might be tempted start our add
function like this:
add : Fin n -> Fin n -> Fin n
add FZ y = y
add (FS x) y = FS (add ?hole1 ?hole2)
This leaves us with two typeholes:
- + Fin.hole1 [P]
`-- x : Fin k
y : Fin (S k)
0 n : Nat
-----------------------
Fin.hole1 : Fin k
- + Fin.hole2 [P]
`-- x : Fin k
y : Fin (S k)
0 n : Nat
-----------------------
Fin.hole2 : Fin k
add
requires two Fin (S k)
terms. We have pattern matched on the first argument and peeled off an FS
, leaving us with x : Fin k
and y : Fin (S k)
.
What we want to do here is make a recursive call on add x y
applied to FS
. This will recurse through the first argument and accumulate FS
constructors until you hit the base case just like Nat
addition.
Fin n = Fin (S k)
but because we wrapped our recursive call in an FS
, we only need to fill the holes with a Fin k
. x : Fin k
fits perfectly as is, however y : Fin (S k)
has too weak of bounds. We need to strengthen (reduce in size) the bounds by one so that it fits the hole.
Strengthening the bounds means reducing the upper limit on a Fin (S
k)
to Fin k
. This is only possible if the actual value is not the upper bound of the set.
Since this is modular addition, what we want to do is strengthen the bound on y
if possible and if it is not possible then we are at the modulus and must replace y
with FZ
.
add : Fin n -> Fin n -> Fin n
add FZ y = y
add (FS x) y =
case ?strengthen y of
Left _ => ?weaken x
Right y' => FS (add x y')
In the case where we wrap around the modulus, we must weaken the constraint on x : Fin k
to account for the fact that we removed an FS
from x
in the pattern match but aren't wrapping an additional FS
in the result.
- + Fin.strengthen [P]
`-- x : Fin k
y : Fin (S k)
0 n : Nat
-------------------------------------------------
Fin.strengthen : Fin (S k) -> Either ?_ (Fin k)
- + Fin.weaken [P]
`-- x : Fin k
y : Fin (S k)
0 n : Nat
---------------------------------
Fin.weaken : Fin k -> Fin (S k)
If we have a way to strengthen and weaken our upper bounds along with a way to detect if we are at the max bound of our Fin _
then we can implement our add
function.
weaken : {n : Nat} -> Fin n -> Fin (S n)
strengthen : {n : Nat} -> Fin (S n) -> Either (Fin (S n)) (Fin n)
weaken
takes any Fin _
and increases its upper bound. This operation is valid for any Fin _
value.
strengthen
takes a Fin (S n)
and reduces the upper bound if possible. If our term x
is the upper bound for Fin (S n)
then we cannot reduce it. To get around this we return an Either
where the Left
case returns the original bound. This allows us to use strengthen
for manipulating our bounds and detecting when we need to wrap around the modulus.
weaken : {n : Nat} -> Fin n -> Fin (S n)
weaken FZ = FZ
weaken (FS k) = FS (weaken k)
For weaken
we recurse through the Fin
structure and rebuild it with the weakend bounds.
strengthen : {n : Nat} -> Fin (S n) -> Either (Fin (S n)) (Fin n)
strengthen {n = Z} FZ = Left FZ
strengthen {n = (S k)} FZ = Right FZ
strengthen {n = Z} (FS _) impossible
strengthen {n = (S k)} (FS x) = case strengthen x of
Left l => Left (FS' l)
Right r => Right (FS' r)
strengthen
is a bit more complex and requires us to use the implicit Nat
value from the type signature. Lets go through the cases one at a time:
{n = Z} FZ
: We have pattern matched on the zeroeth element of the Fin (S Z)
set which means we cannot strengthen and must return FZ : Fin (S Z)
.{n = (S k)} FZ
: We have pattern matched on the zeroeth element of the Fin (S k)
set. k
cannot be Z
due to the previous pattern match so the set must be size 2 or greater allowing us to strengthen.{n = Z} (FS _)
: This case is impossible to reach because FS _
by definition cannot have type Fin n
.{n = (S k)} (FS x)
: Here we have pattern matched on a FS
with a valid Fin n
type. We want to recurse through the Fin
's structure until we hit FZ
to determine if we can strengthen.We now have the tools we need to finish add
:
total
add : Fin n -> Fin n -> Fin n
add FZ y = y
add (FS x) y =
case strengthen y of
Left _ => weaken x
Right y' => FS (add x y')
Here you can see (2 + 3) mod 10 = 5
and (2 + 9) mod 10 = 1
:
> add (the (Fin 10) 2) (the (Fin 10) 3)
λΠFS (FS (FS (FS (FS FZ)))) : Fin 10
> add (the (Fin 10) 2) (the (Fin 10) 9)
λΠFS FZ : Fin 10
This implementation works but lets take this opportunity to generalize our recursion a bit. This will make it easier to implement the rest of our modular arithmetic api.
We can tease apart the recursion from the addition by identify the Recursion Principle at play in our recursive data type and introducing an eliminator function for it. This will allow us to write simple non recursive functions which we can apply to the recursion principle to deconstruct our Fin _
values and produce some result.
The Recursion Principle for a recursive type is a function (t : Type)
-> r
which deconstructs a value of t : Type
into some r
. The Recursion Principle captures the essence of a recursive function on t
and will recursively call itself on every layer of t
inside of t
.
This allows us to traverse through the structure of t
and use some other non-recursive function(s) to accumulate r
. The shape of these non-recursive functions is based on the constructors for t
and their signatures can be derived mechanically.
We do this by replacing all recursive references to t
in the constructor definitions with r
and then for each constructor write a function from its modified parameters to r
.
Lets use foldr : (a -> r -> r) -> r -> List a -> r
as an example. Looking at the definition of List a
:
data List a = Cons a (List a) | Nil
We can start with Nil
and say that in order to eliminate Nil
we need a function () -> r
. Since this is isomorphic to r
, we can simply call this r
.
For Cons
we have a pair of a
and List a
. We replace all recursive instances with r
leaving us with (a, r) -> r
, or a -> r
-> r
in curried form.
Now we pass in those two functions as parameters to our recursive function returning r
and we get foldr : (a -> r -> r) -> r -> List
a -> r
!
We can also write the Recursion Principles for Nat
and Fin
:
data Nat = Z | S Nat
total
nat_rec : (r -> r) -> r -> Nat -> r
nat_rec f r Z = r
nat_rec f r (S k) = nat_rec f (f r) k
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
total
fin_rec : {n : Nat} -> (r -> r) -> r -> Fin r -> a
fin_rec {n = Z} f x y impossible
fin_rec {n = (S k)} f x FZ = x
fin_rec {n = (S k)} f x (FS y) = fin_rec f (f x) y
Notice how if you partially apply Nat
(or Fin
) they look like church numerals. In some sense you can say that the Recursion Principle for a type is equivalent to that type.
Also notice that fin_rec
and nat_rec
are total. Since they take non-recursive functions as parameters this means that whatever functions we implement with our Recursion Principles will also be total.
The basic idea of how to use fin_rec
is that it will recursively call the r -> r
function on the r
value as it traverses through the Fin
constructors. eg., if we have Fin 7
then r -> r
will be applied 7 times.
Armed with fin_rec
we now have the ability to recurse over Fin _
values in a way we know will terminate.
For modular addition we want to specialize r
to Fin n
and use a function Fin n -> Fin n
which increments our intial Fin n
value by one and which accounts for the modulus.
total
inc : {n : Nat} -> Fin n -> Fin n
inc {n = Z} x impossible
inc {n = (S k)} x = case strengthen x of
Left l => FZ
Right r => FS r
Then we kick off our fin_rec
call with the second Fin n
as the initial value and we are done!
total
add : {n : Nat} -> Fin n -> Fin n -> Fin n
add x y = fin_rec inc y x
This function will add 1 to y
x
many times!
Just as with Nat
, multiplication is defined in terms of addition. We use fin_rec
with a partially applied add y
to add y
to FZ
x
times.
total
mul : {n : Nat} -> Fin n -> Fin n -> Fin n
mul {n = Z} x y impossible
mul {n = (S k)} x y = fin_rec (add y) FZ x
We have to pattern match on the implicit value here to prove that the n = 0
case is impossible because of the explicit FZ
value.
In ordinary arithmetic inversion means to find x
for some y
such that y + x = 0
. For example, 2 + (-2) = 0
. It works the same way in modular arithmetic but the results can be a little confusing.
Suppose are working with modulus 5 and we want the inversion of 2. This would be 2 + 3 = 0 (mod 5)
, making the inversion of 2 to be 3!
A simple way of calculating this is to count down from the modulus value until you hit the number to be inverted. The count will be the inversion.
If we had a function decr : {n : Nat} -> Fin n -> Fin n
that behaved exactly like inc
but it decremented by 1 then we could use fin_rec
to decrement down from the modulus x
times where x
is the number we wish to invert.
-- return the largest element in a Fin
total
last : Fin (S n)
last {n = Z} = FZ
last {n = (S k)} = FS last
decr : {n : Nat} -> Fin n -> Fin n
decr {n = 0} x impossible
decr {n = (S k)} FZ = last
decr {n = (S k)} (FS x) = weaken x
total
inv : {n : Nat} -> Fin n -> Fin n
inv {n = 0} x impossible
inv {n = (S k)} x = fin_rec decr FZ x
λΠ> inv (the (Fin 5) 2)
FS (FS (FS FZ))
And there we have it. Our modular arithmetic library:
total
inc : {n : Nat} -> Fin n -> Fin n
inc {n = Z} x impossible
inc {n = (S k)} x = case strengthen x of
Left l => FZ
Right r => FS r
decr : {n : Nat} -> Fin n -> Fin n
decr {n = 0} x impossible
decr {n = (S k)} FZ = last
decr {n = (S k)} (FS x) = weaken x
total
fin_rec : {n : Nat} -> {a : Type} -> (a -> a) -> a -> Fin n -> a
fin_rec {n = Z} f x y impossible
fin_rec {n = (S k)} f x FZ = x
fin_rec {n = (S k)} f x (FS y) = fin_rec f (f x) y
total
add : {n : Nat} -> Fin n -> Fin n -> Fin n
add x y = fin_rec inc y x
total
mul : {n : Nat} -> Fin n -> Fin n -> Fin n
mul {n = Z} x y impossible
mul {n = (S k)} x y = fin_rec (add y) FZ x
total
inv : {n : Nat} -> Fin n -> Fin n
inv {n = 0} x impossible
inv {n = (S k)} x = fin_rec decr FZ x
Writing proofs for associativity, commutivity, etc of modular arithmetic is rather complex and will have to be explored another day.