Messing around with GHC's DataKinds, I tried implementing type-level binary nats. They're simple enough to implement, but if I want them to be useful in common cases, then GHC needs to believe that the Succ
type family is injective (so that type inference will work at least as well as it does for unary type-level nats). However, I'm having a difficult time convincing GHC that this is the case. Consider the following encoding of binary nats:
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilyDependencies #-}
module Nat2 where
data Nat = Zero | Pos Nat1
data Nat1 = One | Bit0 Nat1 | Bit1 Nat1
-- by separating Nat and Nat1 we avoid dealing w/ Bit0^n Zero as a spurrious representation of 0
type family Succ (n :: Nat) :: r | r -> n where
Succ 'Zero = 'Pos 'One
Succ ('Pos x) = 'Pos (Succ1 x)
type family Succ1 (n :: Nat1) :: r | r -> n where
Succ1 'One = 'Bit0 'One
Succ1 ('Bit0 x) = 'Bit1 x
Succ1 ('Bit1 x) = 'Bit0 (Succ1 x)
GHC can't accept this, because it can't tell that Succ1
is injective:
Nat2.hs:12:3: error:
• Type family equations violate injectivity annotation:
Succ 'Zero = 'Pos 'One -- Defined at Nat2.hs:12:3
Succ ('Pos x) = 'Pos (Succ1 x) -- Defined at Nat2.hs:13:3
• In the equations for closed type family ‘Succ’
In the type family declaration for ‘Succ’
|
12 | Succ 'Zero = 'Pos 'One
| ^^^^^^^^^^^^^^^^^^^^^^
Nat2.hs:16:3: error:
• Type family equations violate injectivity annotation:
Succ1 'One = 'Bit0 'One -- Defined at Nat2.hs:16:3
Succ1 ('Bit1 x) = 'Bit0 (Succ1 x) -- Defined at Nat2.hs:18:3
• In the equations for closed type family ‘Succ1’
In the type family declaration for ‘Succ1’
|
16 | Succ1 'One = 'Bit0 'One
| ^^^^^^^^^^^^^^^^^^^^^^^
We think it's injective because, by inspection of Succ1
, One
is never in its image, so the Bit0 (Succ1 x)
case never returns Bit0 One
, and thus the Bit1
case will never conflict with the One
case. The same argument ("One
is not in the image of Succ1
") shows that Succ
itself is also injective. However, GHC is not quite clever enough to find this argument.
So, question: in this case (and cases like these), is there any way to convince GHC that an injective type family is in fact injective? (Perhaps a typechecker plugin where I can provide a function that inverts Succ1? Perhaps a pragma that says "try inferring backwards from this family as if it were injective; if you hit any ambiguities you're allowed to crash"?)