Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing a generic functor instance across type constructors?

I'm learning basic type classes and have written my own implementation of functor for my type Test a (behaves just like Maybe):

data Test a = Test a | Emp

class FC c a where
  t :: (a -> b) -> c a -> c b

instance FC Test a where
  t f (Test a) = Test (f a) 
  t f (Emp) = Emp

instance FC Maybe a where
  t f (Just a) = Just (f a) 
  t f (Nothing) = Nothing

Is it possible to implement something like:

instance FC c where
  t f (c v) = c (f v)

Error:

Parse error in pattern: c

In other words, abstract away the type constructor, replace with c and v, therefore creating a general instance that can be applied to any value with a context?

like image 355
Babra Cunningham Avatar asked Jul 12 '26 19:07

Babra Cunningham


2 Answers

As you've learned, c a is not a syntactically valid pattern. But reading your question instead as a feature proposal: How would that work? Not every Functor has a single-element constructor which can be mapped over according to your pattern. Some examples:

data Pair a = Pair a a  -- more than one element
instance Functor Pair where
    fmap f (Pair x y) = Pair (f x) (f y)

data Proxy a = Proxy  -- no elements
instance Functor Proxy where
    fmap f Proxy = Proxy

newtype Cont r a = Cont { runCont :: (a -> r) -> r }  -- element appears in a double-negative position
instance Functor (Cont r) where
    fmap f (Cont g) = Cont (g . (. f))

In any case, I don't think the idea of a "generic instance" really makes sense. The instance is where you put your type-specific code. (It has to go somewhere!)

If you want to exert less effort in writing Functor instances you can use GHC's DeriveFunctor extension.

{-# LANGUAGE DeriveFunctor #-}

data Pair a = Pair a a deriving Functor

data Proxy a = Proxy deriving Functor

newtype Cont r a = Cont { runCont :: (a -> r) -> r } deriving Functor
like image 84
Benjamin Hodgson Avatar answered Jul 15 '26 14:07

Benjamin Hodgson


You can do something very generic using GHC.Generic. Here is an incomplete example for a generic FC class definition (this is exactly what the generic-deriving package does):

First some extensions and importing the generics machinery

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}

import GHC.Generics

Then we define a class which mirrors your FC but we only have instances for the generic types

class GFC c where
  gt :: (a -> b) -> c a -> c b

-- Constructors without arguments (Empty)
instance GFC U1 where
  gt _ U1 = U1

-- Constructors where the parameter appears (Test a)
instance GFC Par1 where
  gt f (Par1 a) = Par1 (f a)

-- Sums (| in datatype definitions)
instance (GFC f, GFC g) => GFC (f :+: g) where
  gt f (L1 a) = L1 (gt f a)
  gt f (R1 a) = R1 (gt f a)

-- Meta information wrapper
instance GFC f => GFC (M1 i c f) where
  gt f (M1 a) = M1 (gt f a)

-- ... the rest of the instances for the generic types here.
-- But these 4 instances are all that is needed for your `Test` type.

Then you can have a default implementation for FC based on the above "generic" FC:

class FC c where
  t :: (a -> b) -> c a -> c b
  default -- DefaultSignatures allows us to do this
    t :: (Generic1 c, GFC (Rep1 c)) => (a -> b) -> c a -> c b
  t f = to1 . gt f . from1 
  -- turn something with Generic1 into its generic representation, 
  -- use the generic `gt` and then turn it back into its actual 
  -- representation

data Test a = Test a | Empty
  deriving (Generic1, Show)

instance FC Test

And it works:

GHCI> t (==0) (Test (1 :: Int))
Test False
like image 28
adamse Avatar answered Jul 15 '26 13:07

adamse