Equality as a Type
module Tutorial.Eq.Eq
import Data.Either
import Data.HList
import Data.Vect
import Data.String
%default total
Imagine, we'd like to concatenate the contents of two CSV files, both of which we stored on disk as tables together with their schemata as shown in our discussion about dependent pairs:
public export
data ColType = I64 | Str | Boolean | Float
public export
Schema : Type
Schema = List ColType
IdrisType : ColType -> Type
IdrisType I64 = Int64
IdrisType Str = String
IdrisType Boolean = Bool
IdrisType Float = Double
Row : Schema -> Type
Row = HList . map IdrisType
record Table where
constructor MkTable
schema : Schema
size : Nat
rows : Vect size (Row schema)
concatTables1 : Table -> Table -> Maybe Table
We will not be able to implement concatTables1
by appending the two row vectors, unless we can somehow verify that the two schemata are identical. "Well," I hear you say, "that shouldn't be a big issue! Just implement Eq
for ColType
". Let's give this a try:
Eq ColType where
I64 == I64 = True
Str == Str = True
Boolean == Boolean = True
Float == Float = True
_ == _ = False
concatTables1 (MkTable s1 m rs1) (MkTable s2 n rs2) = case s1 == s2 of
True => ?what_now
False => Nothing
Somehow, this doesn't seem to work. If we inspect the context of hole what_now
, Idris still thinks that s1
and s2
are different, and if we go ahead and invoke Vect.(++)
anyway in the True
case, Idris will respond with a type error.
Tutorial.Relations> :t what_now
m : Nat
s1 : List ColType
rs1 : Vect m (HList (map IdrisType s1))
n : Nat
s2 : List ColType
rs2 : Vect n (HList (map IdrisType s2))
------------------------------
what_now : Maybe Table
The problem is, that there is no reason for Idris to unify the two values, even though (==)
returned True
because the result of (==)
holds no other information than the type being a Bool
. We think, if this is True
the two values should be identical, but Idris is not convinced. In fact, the following implementation of Eq ColType
would be perfectly fine as far as the type checker is concerned:
Eq ColType where
_ == _ = True
So Idris is right in not trusting us. You might expect it to inspect the implementation of (==)
and figure out on its own, what the True
result means, but this is not how these things work in general, because most of the time the number of computational paths to check would be far too large. As a consequence, Idris is able to evaluate functions during unification, but it will not trace back information about function arguments from a function's result for us. We can do so manually, however, as we will see later.
A Type for equal Schemata
The problem described above is similar to what we saw when we talked about the benefit of singleton types: The types are not precise enough. What we are going to do now, is something we'll repeat time again for different use cases: We encode a contract between values in an indexed data type:
public export
data SameSchema : (s1 : Schema) -> (s2 : Schema) -> Type where
Same : SameSchema s s
First, note how SameSchema
is a family of types indexed over two values of type Schema
. But note also that the sole constructor restricts the values we allow for s1
and s2
: The two indices must be identical.
Why is this useful? Well, imagine we had a function for checking the equality of two schemata, which would try and return a value of type SameSchema s1 s2
:
sameSchema : (s1, s2 : Schema) -> Maybe (SameSchema s1 s2)
We could then use this function to implement concatTables
:
concatTables : Table -> Table -> Maybe Table
concatTables (MkTable s1 m rs1) (MkTable s2 n rs2) = case sameSchema s1 s2 of
Just Same => Just $ MkTable s1 _ (rs1 ++ rs2)
Nothing => Nothing
It worked! What's going on here? Well, let's inspect the types involved:
concatTables2 : Table -> Table -> Maybe Table
concatTables2 (MkTable s1 m rs1) (MkTable s2 n rs2) = case sameSchema s1 s2 of
Just Same => ?almost_there
Nothing => Nothing
At the REPL, we get the following context for almost_there
:
Tutorial.Relations> :t almost_there
m : Nat
s2 : List ColType
rs1 : Vect m (HList (map IdrisType s2))
n : Nat
rs2 : Vect n (HList (map IdrisType s2))
s1 : List ColType
------------------------------
almost_there : Maybe Table
See, how the types of rs1
and rs2
unify? Value Same
, coming as the result of sameSchema s1 s2
, is a witness that s1
and s2
are actually identical, because this is what we specified in the definition of Same
.
All that remains to do is to implement sameSchema
. For this, we will write another data type for specifying when two values of type ColType
are identical:
public export
data SameColType : (c1, c2 : ColType) -> Type where
SameCT : SameColType c1 c1
We can now define several utility functions. First, one for figuring out if two column types are identical:
sameColType : (c1, c2 : ColType) -> Maybe (SameColType c1 c2)
sameColType I64 I64 = Just SameCT
sameColType Str Str = Just SameCT
sameColType Boolean Boolean = Just SameCT
sameColType Float Float = Just SameCT
sameColType _ _ = Nothing
This will convince Idris, because in each pattern match, the return type will be adjusted according to the values we matched on. For instance, on the first line, the output type is Maybe (SameColType I64 I64)
as you can easily verify yourself by inserting a hole and checking its type at the REPL.
We will need two additional utilities: Functions for creating values of type SameSchema
for the nil and cons cases. Please note, how the implementations are trivial. Still, we often have to quickly write such small proofs (I'll explain in the next section, why I call them proofs), which will then be used to convince the type checker about some fact we already take for granted but Idris does not.
sameNil : SameSchema [] []
sameNil = Same
sameCons : SameColType c1 c2
-> SameSchema s1 s2
-> SameSchema (c1 :: s1) (c2 :: s2)
sameCons SameCT Same = Same
As usual, it can help understanding what's going on by replacing the right hand side of sameCons
with a hole an check out its type and context at the REPL. The presence of values SameCT
and Same
on the left hand side forces Idris to unify c1
and c2
as well as s1
and s2
, from which the unification of c1 :: s1
and c2 :: s2
immediately follows. With these, we can finally implement sameSchema
:
sameSchema [] [] = Just sameNil
sameSchema (x :: xs) (y :: ys) =
[| sameCons (sameColType x y) (sameSchema xs ys) |]
sameSchema (x :: xs) [] = Nothing
sameSchema [] (x :: xs) = Nothing
What we described here is a far stronger form of equality than what is provided by interface Eq
and the (==)
operator: Equality of values that is accepted by the type checker when trying to unify type level indices. This is also called propositional equality: We will see below, that we can view types as mathematical propositions, and values of these types a proofs that these propositions hold.
Type Equal
Propositional equality is such a fundamental concept, that the Prelude exports a general data type for this already: Equal
, with its only data constructor Refl
. In addition, there is a built-in operator for expressing propositional equality, which gets desugared to Equal
: (=)
. This can sometimes lead to some confusion, because the equals symbol is also used for definitional equality: Describing in function implementations that the left-hand side and right-hand side are defined to be equal. If you want to disambiguate propositional from definitional equality, you can also use operator (===)
for the former.
Here is another implementation of concatTables
:
eqColType : (c1,c2 : ColType) -> Maybe (c1 = c2)
eqColType I64 I64 = Just Refl
eqColType Str Str = Just Refl
eqColType Boolean Boolean = Just Refl
eqColType Float Float = Just Refl
eqColType _ _ = Nothing
eqCons : {0 c1,c2 : a}
-> {0 s1,s2 : List a}
-> c1 = c2 -> s1 = s2 -> c1 :: s1 = c2 :: s2
eqCons Refl Refl = Refl
eqSchema : (s1,s2 : Schema) -> Maybe (s1 = s2)
eqSchema [] [] = Just Refl
eqSchema (x :: xs) (y :: ys) = [| eqCons (eqColType x y) (eqSchema xs ys) |]
eqSchema (x :: xs) [] = Nothing
eqSchema [] (x :: xs) = Nothing
concatTables3 : Table -> Table -> Maybe Table
concatTables3 (MkTable s1 m rs1) (MkTable s2 n rs2) = case eqSchema s1 s2 of
Just Refl => Just $ MkTable _ _ (rs1 ++ rs2)
Nothing => Nothing