You don't need the case for adding HZero
and HZero
. This is already covered by the second case. Think how you'd add Peano naturals on the term level, by induction on the first argument:
data Nat = Zero | Succ Nat
add :: Nat -> Nat -> Nat
add Zero y = y
add (Succ x) y = Succ (add x y)
Now if you're using functional dependencies, you're writing a logic program. So instead of making a recursive call on the right hand side, you add a constraint for the result of the recursive call on the left:
class (HNat x, HNat y, HNat r) => HAdd x y r | x y -> r
instance (HNat y) => HAdd HZero y y
instance (HAdd x y r) => HAdd (HSucc x) y (HSucc r)
You don't need the HNat
constraints in the second instance. They're implied by the superclass constraints on the class.
These days, I think the nicest way of doing this sort of type-level programming is to use DataKinds
and TypeFamilies
. You define just as on the term level:
data Nat = Zero | Succ Nat
You can then use Nat
not only as a type, but also as a kind. You can then define a type family for addition on two natural numbers as follows:
type family Add (x :: Nat) (y :: Nat) :: Nat
type instance Add Zero y = y
type instance Add (Succ x) y = Succ (Add x y)
This is much closer to the term-level definition of addition. Also, using the "promoted" kind Nat
saves you from having to define a class such as HNat
.