"Lifting" comes up many times in functional programming, not only in fmap
but in many other contexts. Examples of "liftings" include:
fmap :: (a -> b) -> F a -> F b
where F
is a functor
cmap :: (b -> a) -> F a -> F b
where F
is a contrafunctor
bind :: (a -> M b) -> M a -> M b
where M
is a monad
ap :: F (a -> b) -> F a -> F b
where F
is an applicative functor
point :: (_ -> a) -> _ -> F a
where F
is a pointed functor
filtMap :: (a -> Maybe b) -> F a -> F b
where F
is a filterable functor
extend :: (M a -> b) -> M a -> M b
where M
is a comonad
Other examples include applicative contrafunctor, filterable contrafunctor, and co-pointed functor.
All these type signatures are similar in one way: they map one kind of function between a
and b
into another kind of function between a
and b
.
In these different cases, the function types are not simply a -> b
but have some kind of "twisted" types: e.g. a -> M b
or F (a -> b)
or M a -> b
or F a -> F b
and so on. However, each time the laws are very similar: twisted function types need to have identity and composition laws, and twisted composition needs to be associative.
For example, for applicative functors, we need to be able to compose functions of type F (a -> b)
. So we need to define a special "twisted" identity function (pure id :: F (a -> a)
) and a "twisted" composition operation, call it apcomp
, with type signature F (a -> b) -> F (b -> c) -> F (a -> c)
. This operation needs to have identity and associativity laws. The ap
operation needs to have identity and composition laws ("twisted identity maps to twisted identity" and "twisted composition maps to twisted composition").
Once we go through all these examples and derive the laws, we can prove that the laws turn out to be the same in all cases, if we formulate the laws via the "twisted" operations.
This is because we can formulate all these operations as functors in the sense of category theory. For example, for the applicative functor, we define two categories: the F-applicative category (objects a
, b
, ..., morphisms F(a -> b)
) and the F-lifted category (objects F a
, F b
, ..., morphisms F a -> F b
). A functor between these two categories requires us to have a lifting of morphisms, ap :: F(a -> b) -> F a -> F b
. The laws of ap
are completely equivalent to the standard laws of that functor.
Similar arguments hold for other typeclasses. We need to define categories, morphisms, composition operations, identity morphisms, and functors in each case. Once we verify that the laws hold, we will see that each of these typeclasses has an associated pair of categories and a functor between them, such that the laws of the typeclass are equivalent to the laws of these categories and the functor.
What have we gained? We have formulated the laws of many typeclasses in the same way (as the laws of categories and functors). This is a great economy of thought: we don't need to memorize all these laws each time; we can just memorize which categories and which functors need to be written down for each typeclass, as long as the methods of the typeclass can be reduced to some kind of "twisted lifting".
In this way, we can say that "liftings" are important and provide an application of category theory in functional programming.
I have made a presentation about this, https://www.youtube.com/watch?v=Zau8CxsfxOo and I'm writing a new free book where all derivations will be shown. https://github.com/winitzki/sofp