Search code examples
idris

Easy Syntactic Equality in Idris


Is there an easy way to write equality (DecEq) instances for data types? For example, I'd like the following to have O(n) lines in its DecEq declaration, where ?p is something simple:

data Foo = A | B | C | D

instance [syntactic] DecEq Foo where
   decEq A A = Yes Refl
   decEq B B = Yes Refl
   decEq C C = Yes Refl
   decEq D D = Yes Refl
   decEq _ _ = No ?p

Solution

  • David Christiansen is working on something to automate this in general, and he is essentially finished; it can be found in his GitHub repository. In the mean time, here's an approach that can take you from O(n^2) cases to O(n) cases in this situation. First, some preliminaries. If you have something with decidable equality, and you have an injection from the type of your choice to that type, then you can make a decision procedure for that type:

    IsInjection : (a -> b) -> Type
    IsInjection {a} f = (x,y : a) -> f x = f y -> x = y
    
    decEqInj : DecEq d => (tToDec : t -> d) ->
                          (isInj : IsInjection tToDec) ->
                          (p, q : t) -> Dec (p = q)
    decEqInj tToDec isInj p q with (decEq (tToDec p) (tToDec q))
      | (Yes prf) = Yes (isInj p q prf) 
      | (No contra) = No (\pq => contra (cong pq)) 
    

    Unfortunately, directly proving that your function is an injection gets you back to O(n^2) cases, but it's generally the case that any function with a retraction is injective:

    retrInj : (f : d -> t) -> (g : t -> d) ->
              ((x : t) -> f (g x) = x) ->
              IsInjection g
    retrInj f g prf x y gxgy =
      let fgxfgy = cong {f} gxgy
          foo = sym $ prf x
          bar = prf y
      in trans foo (trans fgxfgy bar)
    

    Thus if you have a function from the type of your choice to one with decidable equality and a retraction for it, then you have decidable equality for your type:

    decEqRet : DecEq d => (decToT : d -> t) ->
               (tToDec : t -> d) ->
               (isRet : (x : t) -> decToT (tToDec x) = x) ->
               (p, q : t) -> Dec (p = q)
    decEqRet decToT tToDec isRet p q =
      decEqInj tToDec (retrInj decToT tToDec isRet) p q
    

    Finally, you can write the cases for what you've chosen:

    data Foo = A | B | C | D
    
    natToFoo : Nat -> Foo
    natToFoo Z = A
    natToFoo (S Z) = B
    natToFoo (S (S Z)) = C
    natToFoo _ = D
    
    fooToNat : Foo -> Nat 
    fooToNat A = 0
    fooToNat B = 1
    fooToNat C = 2
    fooToNat D = 3
    
    fooNatFoo : (x : Foo) -> natToFoo (fooToNat x) = x
    fooNatFoo A = Refl
    fooNatFoo B = Refl
    fooNatFoo C = Refl
    fooNatFoo D = Refl
    
    instance DecEq Foo where
      decEq x y = decEqRet natToFoo fooToNat fooNatFoo x y
    

    Note that while the natToFoo function has somewhat large patterns, there isn't really much going on there. It should probably be possible to make the patterns small by nesting them, although that may be ugly.

    Generalization: At first I thought this would only work for special cases, but I now think it may be a little better than that. In particular, if you have an algebraic datatype holding types with decidable equality, you should be able to convert it into a nested Either of nested Pair, which will get you there. For instance (using Maybe to shorten Either (Bool, Nat) ()):

    data Fish = Cod Int | Trout Bool Nat | Flounder
    
    watToFish : Either Int (Maybe (Bool, Nat)) -> Fish
    watToFish (Left x) = Cod x
    watToFish (Right Nothing) = Flounder
    watToFish (Right (Just (a, b))) = Trout a b
    
    fishToWat : Fish -> Either Int (Maybe (Bool, Nat))
    fishToWat (Cod x) = Left x
    fishToWat (Trout x k) = Right (Just (x, k))
    fishToWat Flounder = Right Nothing
    
    fishWatFish : (x : Fish) -> watToFish (fishToWat x) = x
    fishWatFish (Cod x) = Refl
    fishWatFish (Trout x k) = Refl
    fishWatFish Flounder = Refl
    
    instance DecEq Fish where
      decEq x y = decEqRet watToFish fishToWat fishWatFish x y