generic-functor
Deriving generalized functors with GHC.Generics
https://gitlab.com/lysxia/generic-functor
Version on this page: | 0.2.0.0@rev:1 |
LTS Haskell 22.43: | 1.1.0.0 |
Stackage Nightly 2024-12-04: | 1.1.0.0 |
Latest on Hackage: | 1.1.0.0 |
generic-functor-0.2.0.0@sha256:bb8d78c551850ba119a9e504fbea8c4b9d0a3d62a1dfa2c0d5444ad58ce3f1c1,1281
Module documentation for 0.2.0.0
Generic functors
Implementation of Functor
instances and other functor-like structures
using GHC.Generics
.
Functors not over the last type parameter
The standard Functor
class only applies to types that are functors over their
last type parameter. For example, in Either e r
, fmap
maps only r
.
Using this library, fmap
-like functions can be derived over any type
parameter of a Generic
data type, all from the same definition gsolomap
.
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics (Generic)
import Generic.Functor (gsolomap)
data Result a r = Error a | Ok r -- Another name for Either
deriving Generic
mapError :: (a -> b) -> Result a r -> Result b r
mapError = gsolomap
-- This one is fmap
mapOk :: (a -> b) -> Result e a -> Result e b
mapOk = gsolomap
mapBoth :: (a -> b) -> Result a a -> Result b b
mapBoth = gsolomap
gsolomap
is unsafe. Misuse will break your program.
Read on for specifics.
Usage
gsolomap
should only be used to define polymorphic “fmap
-like functions”
for Generic
types.
The signature of gsolomap
is:
gsolomap :: (Generic x, Generic y, GSolomap a b x y) => (a -> b) -> (x -> y)
The types x
and y
must be specializations of the same user-defined data
type which is an instance of Generic
, with some type parameters equal to a
or b
respectively. At use sites of gsolomap
, a
and b
must also be two
distinct universally quantified type variables, with no equality constraint
relating them with each other or any other type.
The guarantee is that gsolomap
satisfies gsolomap id = id
. Under the
condition that a
and b
are abstract, that equation uniquely determines the
implementation. (That uniqueness claim may be broken with GADTs and other
explicit uses of type equality constraints.)
In particular, gsolomap
must not be specialized with types a
and b
that
are equal. A function defined using gsolomap
is safe to specialize once
the GSolomap
constraint has been discharged.
For instance the three functions above, mapError
, mapOk
, mapBoth
are
sufficiently polymorphic.
They are each uniquely determined by their types and the equation mapX id = id
.
(Without that equation, mapBoth
has four implementations of the same type.)
Compositions of functors
How many fmap
do you need to map a function a -> b
over
(t, Maybe [Either Bool a])
?
You only need one solomap
:
type F t a = (t, Maybe [Either Bool a])
maps :: (a -> b) -> F t a -> F t b
maps = solomap
solomap
can also see through bifunctors and there may be more than
one occurrence of the type parameter a
.
type F a = ([a], Either a ())
maps2 :: (a -> b) -> F a -> F b
maps2 = solomap
solomap
is unsafe, subject to the same restrictions as gsolomap
:
where solomap
is used, the type of its first argument (a -> b)
must refer
to two distinct universally quantified variables a
and b
.
Functions are safe to specialize only once the Solomap
constraint is out of
their contexts.
solomap :: Solomap a b x y => (a -> b) -> (x -> y)
Functors of multiple parameters
You can also map with more than one function simultaneously.
For example with a -> b
and c -> d
over (Maybe a, [(c, a)])
:
type F a c = (Maybe a, [(c, a)])
bimaps :: (a -> b) -> (c -> d) -> F a c -> F b d
bimaps f g = multimap (f :+ g :+ ())
multimap
takes a list of functions separated by (:+)
and terminated by ()
.
There is also a gmultimap
, generalizing gsolomap
.
gmultimap
and multimap
are unsafe, similarly to gsolomap
and solomap
.
Deriving Functor
This library enables DerivingVia
for the Functor
class.
{-# LANGUAGE DeriveGeneric, DerivingVia #-}
import GHC.Generics (Generic)
import Generic.Functor (GenericFunctor(..))
data Twice a = Twice (Either a a)
deriving Generic
deriving Functor via (GenericFunctor Twice)
Note that there is already built-in support for deriving Functor
in GHC with the
DeriveFunctor
extension instead. If that extension ever chokes on a type, this
library might have a chance at handling it. (Open an issue if it does not!)
The Twice
example just above is not handled by the DeriveFunctor
extension:
{-# LANGUAGE DeriveFunctor #-}
data Twice a = Twice (Either a a) deriving Functor
{-
error:
• Can't make a derived instance of ‘Functor Twice’:
Constructor ‘Twice’ must use the type variable only as the last argument of a data type
-}
The generic-data library also includes a generic implementation of Functor
,
but only for instances of Generic1
, which applies to much more restricted shapes
of data
than Generic
.
Deriving Bifunctor
Similarly, we can use DerivingVia
for the Bifunctor
class
(from base, module Data.Bifunctor
).
{-# LANGUAGE DeriveGeneric, DerivingVia #-}
import GHC.Generics (Generic)
import Generic.Functor (GenericFunctor(..), GenericBifunctor(..))
data Tree a b = Node a (Tree a b) (Tree a b) | Leaf b
deriving Generic
deriving Functor via (GenericFunctor (Tree a))
deriving Bifunctor via (GenericBifunctor Tree)
In summary, the newtype GenericFunctor
can be used for DerivingVia
of the classes Functor
and Foldable
, and the newtype GenericBifunctor
for the classes Bifunctor
and Bifoldable
.
Default implementations for the above classes are also available as standalone
functions (gfmap
, gfoldMap
, gbimap
, gbifoldMap
) and also for
Traversable
and Bitraversable
(gtraverse
, gbitraverse
).
Internal module policy
The public API is Generic.Functor
. Don’t use Generic.Functor.Internal
.
Future work
- Functors in arbitrary categories.
Related links
-
generic-data: utilities for
GHC.Generics
and deriving for other standard classes. -
generic-lens: the
params
traversal uses a very similar implementation. -
Deriving Bifunctors with Generics, blogpost by Csongor Kiss, describing the main idea for the implementation (using incoherent instances).
Changes
0.2.0.0
- Add
gfoldMap
,gtraverse
,gbifoldMap
,gbitraverse
GFoldable
,GFoldMap
,GTraversable
,GTraverse
,GBiFoldable
,GBifoldMap
,GBitraversable
,GBitraverse
- Rename
GBifunctor
toGBimap
, and add newGBifunctor
(class synonym forGBimap
,GFirst
, andGSecond
) - Rename
DeriveFunctor
toGenericFunctor
, and renameDeriveBifunctor
toGenericBifunctor
- Add instances for deriving-via
Foldable
andBifoldable
0.1.0.0
- Add
gmultimap
,multimap
,(:+)
- Add
DeriveBifunctor
,gbimap
,gfirst
,gsecond
0.0.1.1
- Include README
0.0.1.0
- Create generic-functor