derive functor · Wiki · Glasgow Haskell Compiler / GHC · GitLab
source link: https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/compiler/derive-functor
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Support for deriving Functor
, Foldable
, and Traversable
instances
GHC 6.12.1 introduces an extension to the deriving
mechanism allowing for automatic derivation of Functor
, Foldable
, and Traversable
instances using the DeriveFunctor
, DeriveFoldable
, and DeriveTraversable
extensions, respectively. Twan van Laarhoven first proposed this feature in 2007, and opened a related GHC ticket in 2009.
Example
The derived code would look something like this:
Algorithm description
DeriveFunctor
, DeriveFoldable
, and DeriveTraversable
all operate using the same underlying mechanism. GHC inspects the arguments of each constructor and derives some operation to perform on each argument, which depends of the type of the argument itself. In a Functor
instance, for example fmap
would be applied to occurrences of the last type parameter, but id
would be applied to other type parameters. Typically, there are five cases to consider. (Suppose we have a data type data A a = ...
.)
- Terms whose type does not mention
a
- Terms whose type mentions
a
- Occurrences of
a
- Tuple values
- Function values
After this is done, the new terms are combined in some way. For instance, Functor
instances combine terms in a derived fmap
definition by applying the appropriate constructor to all terms, whereas in Foldable
instances, a derived foldMap
definition would mappend
the terms together.
DeriveFunctor
A comment in TcGenDeriv.hs lays out the basic structure of DeriveFunctor
, which derives an implementation for fmap
.
DeriveFunctor
is special in that it can recurse into function types, whereas DeriveFoldable
and DeriveTraversable
cannot (see the section on covariant and contravariant positions).
DeriveFoldable
Another comment in TcGenDeriv.hs reveals the underlying mechanism behind DeriveFoldable
:
In addition to foldr
, DeriveFoldable
also generates a definition for foldMap
as of GHC 7.8.1 (addressing #7436 (closed)). The pseudo-definition for $(foldMap)
would look something like this:
DeriveTraversable
From TcGenDeriv.hs:
Covariant and contravariant positions
One challenge of deriving Functor
instances for arbitrary data types is handling function types. To illustrate this, note that these all can have derived Functor
instances:
but none of these can:
In CovFun1
, CovFun2
, and CovFun3
, all occurrences of the type variable a
are in covariant positions (i.e., the a
values are produced), whereas in ContraFun1
, ContraFun2
, and ContraFun3
, all occurrences of a
are in contravariant positions (i.e., the a
values are consumed). If we have a function f :: a -> b
, we can't apply f
to an a
value in a contravariant position, which precludes a Functor
instance.
Most type variables appear in covariant positions. Functions are special in that the lefthand side of a function arrow reverses variance. If a function type a -> b
appears in a covariant position (e.g., CovFun1
above), then a
is in a contravariant position and b
is in a covariant position. Similarly, if a -> b
appears in a contravariant position (e.g., CovFun2
above), then a
is in a covariant position and b
is in a contravariant position.
If we annotate covariant positions with p
(for positive) and contravariant positions with n
(for negative), then we can examine the above examples with the following pseudo-type signatures:
Since ContraFun1
, ContraFun2
, and ContraFun3
all use the last type parameter in at least one n
position, GHC would reject a derived Functor
instance for each of them.
Requirements for legal instances
This mechanism cannot derive Functor
, Foldable
, or Traversable
instances for all data types. Currently, GHC checks if a data type meets the following criteria:
- The data type has at least one type parameter. (For example,
data NoArg = NoArg
cannot have aFunctor
instance.) - The data type's last type parameter cannot be used contravariantly. (see the section on covariant and contravariant positions.)
- The data type's last type parameter cannot be used in the "wrong place" in any constructor's data arguments. For example, in
data Right a = Right [a] (Either Int a)
, the type parametera
is only ever used as the last type argument in[]
andEither
, so both[a]
andEither Int a
values can befmap
ped. However, indata Wrong a = Wrong (Either a a)
, the type variablea
appears in a position other than the last, so trying tofmap
anEither a a
value would not typecheck.
Note that there are two exceptions to this rule: tuple and function types.
- The data type's last type variable cannot used in a
-XDatatypeContexts
constraint. For example,data Ord a => O a = O a deriving Functor
would be rejected.
In addition, GHC performs checks for certain classes only:
- For derived
Foldable
andTraversable
instances, a data type cannot use function types. This restriction does not apply to derivedFunctor
instances, however. - For derived
Functor
andTraversable
instances, the data type's last type variable must be truly universally quantified, i.e., it must not have any class or equality constraints. This means that the following is legal:
but the following is not legal:
This restriction does not apply to derived Foldable
instances. See the following section for more details.
Relaxed universality check for DeriveFoldable
DeriveFunctor
and DeriveTraversable
cannot be used with data types that use existential constraints, since the type signatures of fmap
and traverse
make this impossible. However, Foldable
instances are unique in that they do not produce constraints, but only consume them. Therefore, it is permissible to derive Foldable
instances for constrained data types (e.g., GADTs).
For example, consider the following GADT:
In the type signatures for fmap :: Functor t => (a -> b) -> t a -> t b
and traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)
, the t
parameter appears both in an argument and the result type, so pattern-matching on a value of t
must not impose any constraints, as neither fmap
nor traverse
would typecheck.
Foldable
, however, only mentions t
in argument types:
Therefore, a derived Foldable
instance for T
typechecks:
Deriving Foldable
instances for GADTs with equality constraints could become murky, however. Consider this GADT:
All four E
constructors have the same "shape" in that they all take an argument of type a
(or Int
, to which a
is constrained to be equal). Does that mean all four constructors would have their arguments folded over? While it is possible to derive perfectly valid code which would do so:
it is much harder to determine which arguments are equivalent to a
. Also consider this case:
For all we know, it may be that a ~ Int => Mystery a
. Does this mean that the Int
argument in UC
should be folded over?
To avoid these thorny edge cases, we only consider constructor arguments (1) whose types are syntactically equivalent to the last type parameter and (2) in cases when the last type parameter is a truly universally polymorphic. In the above E
example, only E1
fits the bill, so the derived Foldable
instance is actually:
To expound more on the meaning of criterion (2), we want not only to avoid cases like E2 :: Int -> E Int
, but also something like this:
In this example, the last type variable is instantiated with f a
, which contains one type variable f
applied to another type variable a
. We would not fold over the argument of type f a
in this case, because the last type variable should be simple, i.e., contain only a single variable without any application.
For the original discussion on this proposal, see #10447 (closed).
Alternative strategy for deriving Foldable
and Traversable
We adapt the algorithms for -XDeriveFoldable
and -XDeriveTraversable
based on that of -XDeriveFunctor
. However, there is an important difference between deriving the former two typeclasses and the latter one (as of GHC 8.2, addressing Trac #11174), which is best illustrated by the following scenario:
The generated code for the Functor
instance is straightforward:
But if we use too similar of a strategy for deriving the Foldable
and Traversable
instances, we end up with this code:
This is unsatisfying for two reasons:
-
The
Traversable
instance doesn't typecheck!Int#
is of kind#
, butpure
expects an argument whose type is of kind*
. This effectively preventsTraversable
from being derived for any datatype with an unlifted argument type (see Trac #11174). -
The generated code contains superfluous expressions. By the
Monoid
laws, we can reducef a <> mempty
tof a
, and by theApplicative
laws, we can reducefmap WithInt (f a) <*> pure i
tofmap (\b -> WithInt b i) (f a)
.
We can fix both of these issues by incorporating a slight twist to the usual algorithm that we use for -XDeriveFunctor
. The differences can be summarized as follows:
-
In the generated expression, we only fold over arguments whose types mention the last type parameter. Any other argument types will simply produce useless
mempty
s orpure
s, so they can be safely ignored. -
In the case of
-XDeriveTraversable
, instead of applyingConName
, we apply\b_i ... b_k -> ConName a_1 ... a_n
, where
-
ConName
hasn
arguments -
{b_i, ..., b_k}
is a subset of{a_1, ..., a_n}
whose indices correspond to the arguments whose types mention the last type parameter. As a consequence, taking the difference of{a_1, ..., a_n}
and{b_i, ..., b_k}
yields the all the argument values ofConName
whose types do not mention the last type parameter. Note that[i, ..., k]
is a strictly increasing—but not necessarily consecutive—integer sequence.For example, the datatype
would generate the following Traversable
instance:
Technically, this approach would also work for -XDeriveFunctor
as well, but we decide not to do so because:
-
There's not much benefit to generating, e.g.,
(\b -> WithInt b i) (f a)
instead ofWithInt (f a) i
. -
There would be certain datatypes for which the above strategy would generate
Functor
code that would fail to typecheck. For example:With the conventional algorithm, it would generate something like:
which typechecks. But with the strategy mentioned above, it would generate:
which does not typecheck, since GHC cannot unify the rank-2 type variables in the types of
b
andfmap f a
.
Future work
There are more classes in base
that we could derive!
In particular, the Bifunctor
class (born from the bifunctors library) was added to base
in GHC 7.10, and the Bifoldable
and Bitraversable
classes (also from bifunctors
) were added to base
in GHC 8.2. All three classes could be derived in much the same way as their cousins Functor
, Foldable
, and Traversable
. The existing algorithms would simply need to be adapted to accommodate two type parameters instead of one. The Data.Bifunctor.TH module from the bifunctors
library demonstrates an implementation of the following proposal using Template Haskell.
In GHC 8.0, higher-order versions of the Eq
, Ord
, Read
, and Show
typeclasses were added to base
in the Data.Functor.Classes
module (which originally lived in the transformers
library). These classes are generalized to work over datatypes indexed by one type parameter (for Eq1
, Ord1
, Read1
, and Show1
) or by two type parameters (Eq2
, Ord2
, Read2
, and Show2
). Haskell programmers have been able to derive Eq
, Ord
, Read
, and Show
for a long time, so it wouldn't be hard at all to envision a deriving mechanism for Eq1
, Eq2
, and friends which takes advantage of tricks that DeriveFunctor
uses. The deriving-compat library demonstrates proofs-of-concept for deriving Eq1/2, Ord1/2, Read1/2, and Show1/2 using Template Haskell.
Classes
The Bifunctor
, Bifoldable
, and Bitraversable
classes are defined as follows:
Each class contains further methods, but they can be defined in terms of the above ones. Therefore, we need only derive implementations for them. This also mirrors how the algorithms currently work in the one-parameter cases, as they only implement fmap
, foldMap
, foldr
, and traverse
.
The typeclasses in Data.Functor.Classes
are defined as follows:
Algorithms
A pseudo-code algorithm for generating a bimap
implementation is:
This algorithm isn't terribly different from the one above for generating an fmap
implementation, and that's the point. It's simply generalizing the same ideas to work over a typeclass of kind * -> * -> *
. The algorithms for generating foldMap
/foldr
and traverse
can be generalized to generate bifoldMap
/bifoldr
and bitraverse
, respectively. For example, here's what the algorithm for bifoldMap
would look like:
(The caveats in https://gitlab.haskell.org/trac/ghc/wiki/Commentary/Compiler/DeriveFunctor#AlternativestrategyforderivingFoldableandTraversable apply.)
There's one part of the bifoldMap
algorithm that deserves futher discussion: the overlapping cases for T c1 c1 c3
. Whenever an argument to a constructor has a type where each of the last two type variables mention a
or b
, we opt to generate bifoldMap
instead of foldMap
. We could go the other way, though. For instance, the following is a valid implementation of Bifoldable
for newtype T a b = T (Either a b)
:
But this is unsatisfying for a couple of reasons, though. One obvious issue is that this definition blatantly ignores the first argument to bifoldMap
, preventing users from folding over the a
type parameter. Another problem is that doing this would be inconsistent with how bimap
and bitraverse
are generated. Unlike with bifoldMap
, parametricity forces there to be one definition for bimap
and bitraverse
(see https://gitlab.haskell.org/trac/ghc/wiki/Commentary/Compiler/DeriveFunctor#RelaxeduniversalitycheckforDeriveFoldable for more info):
Therefore, it feels far more natural to generate this Bifoldable
instance:
This also ensures that bifoldMapDefault gives the same result as bifoldMap
.
Corner case: GADTs
Consider the following code:
What should be the definition of bifoldMap
for Both
? We have a choice, since both the function argument of type (a -> m)
and of type (b -> m)
can be applied to either argument. In such a scenario, the second fold function takes precedence over the first fold function, so the derived Bifoldable
instance would be:
This definition ensures that bifoldMap id = foldMap
for a derived Foldable
instance for Both
.
Data.Functor.Classes
Deriving Eq1/2
, Ord1/2
, and Show1/2
could be done in a very similar way to deriving Foldable/Bifoldable
. For instance, you can substitute in liftEq
/liftEq2
in place of foldMap
/bifoldMap
in the above algorithm and have a sensible way to generate liftEq
/liftEq2
expressions. In addition, Eq1/2
, Ord1/2
, and Show1/2
can all be derived for GADTs as well.
Read1/2
could not be derived for GADTs, since it mentions type variables in the return types of its methods, but it could still be derived using the same principles as deriving Foldable
/Bifoldable
.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK