Writing a generic functor instance across type constructors?

旧街凉风 提交于 2019-12-07 18:18:17

问题


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?


回答1:


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



回答2:


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



回答3:


As far as I know this is not possible, simply because there can be multiple constructors and it is unknown whether a generic constructor Foo can take any attribute as type.

Say for instance you have a type called:

data Foo a = Bar Int | Qux a

Now it means you cannot abstract away the constructor. As long as it is Qux, there is no problem, but Bar always expects an Int and thus will error. Since you here define an instance over any kind of c, there will be cases where this does not work. Note furthermore that the c in your instance declaration has nothing to do with the c in your definition of t. In other words: constructors can imply type constraints so you cannot simply factor them out.

A remark on your question is that you can generalize both you class defintion and instance:

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

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

So you can remove the a in the class definition. This is not equivalent with your questions since here you say that it can work for any a. Whereas when you define a class FC c a you can decide for which as you want implement an instance.



来源:https://stackoverflow.com/questions/42026435/writing-a-generic-functor-instance-across-type-constructors

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!