I'm using Fix to define some datatypes for a project I'm working on. I need those datatypes to be instances of the equality typeclass Eq for unit testing. I'm stumped on how to write an instance of Eq for Fix or more specifically List defined in terms of Fix.
My Fix:
-- Fixed point of a Functor
newtype Fix f = In (f (Fix f))
out :: Fix f -> f (Fix f)
out (In f) = f
My List in terms of Fix:
data ListF a r = NilF | ConsF a r
instance Functor (ListF a) where
fmap _ NilF = NilF
fmap f (ConsF a r) = ConsF a (f r)
type ListF' a = Fix (ListF a)
CodePudding user response:
One possible approach is using undecidable instances to create "recursive instances", so to speak.
{-# LANGUAGE UndecidableInstances #-}
-- Fixed point of a Functor
newtype Fix f = In (f (Fix f))
instance Show (f (Fix f)) => Show (Fix f) where
showsPrec p (In x) = showParen (p > 10) $
("In " ) . showsPrec 11 x
data ListF a r = NilF | ConsF a r
deriving Show
Above, we define showsPrec so to add parentheses only when needed. The magic number 10 corresponds to the application precedence level, and showParen conditionally adds parentheses.
Note that the functor must be declared with deriving Show for this to work.
Note the tricky constraint resolution here. When checking Show ListF', GHC reduces that to Show (Fix ListF), then to Show (ListF (Fix ListF)), then to Show (Fix ListF) again but now "ties the knot" and produces a "recursive instance" instead of getting stuck in an infinite loop.
Alternatively, QuantifiedConstraints also works, avoiding the instance-level recursion. (We still need undecidable instances, though.)
instance (forall a. Show a => Show (f a))
=> Show (Fix f) where
showsPrec p (In x) = showParen (p > 10) $
("In " ) . showsPrec 11 x
CodePudding user response:
@chi's answer is great but gives you a Show instance instead of an Eq instance.
Writing an Eq instance directly for ListF' is actually pretty straightforward. You were probably just overthinking it:
instance Eq a => Eq (ListF' a) where
In (ConsF a as) == In (ConsF b bs) | a == b, as == bs = True
In NilF == In NilF = True
In _ == In _ = False
You can stop there, but if you factor out the newtype unwrapping:
instance Eq a => Eq (ListF' a) where
In f == In g = eqListF f g
where eqListF (ConsF a as) (ConsF b bs) | a == b, as == bs = True
eqListF NilF NilF = True
eqListF _ _ = False
and turn eqListF into an instance:
instance Eq a => Eq (ListF' a) where
In f == In g = f == g
instance (Eq a, Eq r) => Eq (ListF a r) where
ConsF a as == ConsF b bs | a == b, as == bs = True
NilF == NilF = True
_ == _ = False
it starts to become clearer how you get to a "generic" solution (for Fix f), though it's tough to figure this out unless you've already seen Eq1 somewhere.
class Eq1 f where
liftEq :: (a -> a -> Bool) -> (f a -> f a -> Bool)
instance Eq1 f => Eq (Fix f) where
In f == In g = liftEq (==) f g
instance (Eq a) => Eq1 (ListF a) where
liftEq (===) (ConsF a as) (ConsF b bs) | a == b, as === bs = True
liftEq _ NilF NilF = True
liftEq _ _ _ = False
Note that the === in the third-last line is bound to the == being lifted by liftEq.
As per @chi's answer, it's actually possible to avoid using the awkward Eq1 instance by taking advantage of the relatively new QuantifiedConstraints extension. The rewritten instances look like this, and the Eq instance for ListF is much more natural than the Eq1 instance above:
instance (forall a. Eq a => Eq (f a)) => Eq (Fix f) where
In f == In g = f == g
instance (Eq a, Eq l) => Eq (ListF a l) where
ConsF a as == ConsF b bs | a == b, as == bs = True
NilF == NilF = True
_ == _ = False
Anyway, here's full code, using the traditional Eq1 approach (and taking the class Eq1 from Data.Functor.Classes).
{-# LANGUAGE FlexibleInstances #-}
import Data.Functor.Classes
newtype Fix f = In { out :: f (Fix f) }
data ListF a r = NilF | ConsF a r
type ListF' a = Fix (ListF a)
instance Eq1 f => Eq (Fix f) where
In f == In g = liftEq (==) f g
instance (Eq a) => Eq1 (ListF a) where
liftEq (===) (ConsF a as) (ConsF b bs) | a == b, as === bs = True
liftEq _ NilF NilF = True
liftEq _ _ _ = False
consF a b = In (ConsF a b)
nilF = In NilF
main = do
print $ nilF == (nilF :: ListF' Char)
print $ consF 'a' nilF == consF 'a' nilF
print $ consF 'a' nilF == consF 'b' nilF
print $ consF 'a' nilF == consF 'a' (consF 'b' nilF)
And here's the QuantifiedConstraints version:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.Functor.Classes
newtype Fix f = In { out :: f (Fix f) }
data ListF a r = NilF | ConsF a r
type ListF' a = Fix (ListF a)
instance (forall a. Eq a => Eq (f a)) => Eq (Fix f) where
In f == In g = f == g
instance (Eq a, Eq l) => Eq (ListF a l) where
ConsF a as == ConsF b bs | a == b, as == bs = True
NilF == NilF = True
_ == _ = False
consF a b = In (ConsF a b)
nilF = In NilF
main = do
print $ nilF == (nilF :: ListF' Char)
print $ consF 'a' nilF == consF 'a' nilF
print $ consF 'a' nilF == consF 'b' nilF
print $ consF 'a' nilF == consF 'a' (consF 'b' nilF)
