Search code examples
haskelltype-inferencetypeclassfunctor

Haskell type classes and instances


Why does the code below require a constraint and the type parameter for an instance of Show but are they need required for making Quad an instance of Functor?

data Quad a = Quad a a a a 
instance (Show a) => Show (Quad a) where
  show (Quad a b c d) = show a ++ " " ++ show b ++ "\n" ++ 
                        show c ++ " " ++ show d
instance Functor Quad where 
  fmap f (Quad a b c d) = Quad (f a) (f b) (f c) (f d) 

Solution

  • Your definition of Show requires show be applied to each value wrapped by the Quad data constructor, which imposes the constraint. It would not be needed if you had a trivial instance like

    instance Show (Quad a) where
        show (Quad a b c d) = "some Quad value"
    

    because this definition doesn't care about the type of a et al.:

    > show (Quad 1 2 3 4)
    "some Quad value"
    > show (Quad (+1) (+2) (+3) (+4))
    "some Quad value"
    

    fmap, on the other hand, has type (a -> b) -> f a -> f b, because fmap itself places no constraint on the type used by Quad; any such constraints are imposed by whatever function is passed to fmap as its first argument:

    > :t fmap
    fmap :: Functor f => (a -> b) -> f a -> f b
    > :t fmap show
    fmap show :: (Functor f, Show a) => f a -> f String
    

    Sometimes, an instance for Functor will require a constraint. For example, consider the Compose type from Data.Functor.Compose:

    data Compare f g = Compose { getCompose :: f (g a) }
    

    Ignoring its name, all it requires is two type constructors with kind Type -> Type. But, if you want a Functor instance for Compose, then those type constructors must also have Functor instances, because we'll use fmap internally.

    instance (Functor f, Functor g) => Functor (Compose f g) where
        -- x :: f (g a)
        fmap f (Compose x) = Compose (fmap (fmap f) x)
    

    For example, fmap (+1) [1,2,3] == [2,3,4], but fmap (+1) [[1,2,3], [4,5,6]] wouldn't typecheck, because (+1) can't take a list as an argument. Compose lets us "dig into" the nested functor.

    -- Compose [[1,2,3],[4,5,6]] :: Num a => Compose [] [] a
    > fmap (+1) (Compose [[1,2,3], [4,5,6]])
    Compose [[2,3,4],[5,6,7]]
    
    -- Compose [Just 3, Just 4, Nothing] :: Num a => Compose [] Maybe a
    > fmap (+1) (Compose [Just 3, Just 4, Nothing])
    Compose [Just 4,Just 5,Nothing]
    
    -- Compose Nothing :: Compose Maybe g a
    > fmap (+1) (Compose Nothing)
    Nothing
    
    -- Compose (Just [1,2,3]) :: Num a => Compose Maybe [] a
    > fmap (+1) (Compose (Just [1,2,3]))
    Compose (Just [2,3,4])