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 #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE UndecidableInstances #-}
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 Succ :: Nat -> r
type family Succ n :: r | r -> n where
Succ 'Zero = 'Pos 'One
Succ ('Pos x) = 'Pos (Succ1 x)
type Succ1 :: Nat1 -> r
type family Succ1 n :: 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:15:3: error: [GHC-05175]
• Type family equation right-hand sides overlap; this violates
the family's injectivity annotation:
Succ Zero = Pos One -- Defined at Nat2.hs:15:3
Succ (Pos x) = Pos (Succ1 x) -- Defined at Nat2.hs:16:3
• In the equations for closed type family ‘Succ’
In the type family declaration for ‘Succ’
|
15 | Succ 'Zero = 'Pos 'One
| ^^^^^^^^^^^^^^^^^^^^^^
Nat2.hs:20:3: error: [GHC-05175]
• Type family equation right-hand sides overlap; this violates
the family's injectivity annotation:
Succ1 One = Bit0 One -- Defined at Nat2.hs:20:3
Succ1 (Bit1 x) = Bit0 (Succ1 x) -- Defined at Nat2.hs:22:3
• In the equations for closed type family ‘Succ1’
In the type family declaration for ‘Succ1’
|
20 | 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"?)
Succ
is injective uses the same key argument as the proof thatSucc1
is injective (namely, "One
is not in the image ofSucc1
"), so I'm hoping that any method that can convince GHC thatSucc1
is injective also works to convince it aboutSucc
. – Handlerdata Bit = One | Two; type Nat = [Bit]
, or perhapsdata Nat = Zero | Bit1 Nat | Bit2 Nat
? If you can't make those work, your chances of making something fancier work seem pretty slim. – Bernettabernettedata Nat = Zero | Bit1 Nat | Bit2 Nat
is that in that case, the functionSucc
isn't injective:Succ Zero
andSucc (Bit0 Zero)
both map toBit1 Zero
. The hope is that, given a representation of binary nats whereSucc
is injective, GHC can be made to believe this (or at least take it on my word). – HandlerBit0
in the representation I mentioned--it's 1-2 rather than 0-1. – Bernettabernette