I understand that many of the names in Haskell are inspired by category theory terminology, and I'm trying to understand exactly where the analogy begins and ends.
The Category Hask
I already know that Hask
is not (necessarily) a category due to some technical details about strictness/laziness and seq
, but let's put that aside for now. For clarity,
- The objects of
Hask
are concrete types, that is, types of kind*
. This includes function types likeInt -> [Char]
, but not anything that requires a type parameter likeMaybe :: * -> *
. However, the concrete typeMaybe Int :: *
belongs toHask
. Type constructors / polymorphic functions are more like natural transformations (or other more general maps fromHask
to itself), rather than morphisms. - The morphisms of
Hask
are Haskell functions. For two concrete typesA
andB
, the hom-setHom(A,B)
is the set of functions with signatureA -> B
. - Function composition is given by
f . g
. If we are worried about strictness, we might redefine composition to be strict or be careful about defining equivalence classes of functions.
Functor
s are Endofunctors in Hask
I don't think the technicalities above have anything to do with my confusion below. I think I understand it means to say that every instance of Functor
is an endofunctor in the category Hask
. Namely, if we have
class Functor (F :: * -> *) where
fmap :: (a -> b) -> F a -> F b
-- Maybe sends type T to (Maybe T)
data Maybe a = Nothing | Just a
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap _ Nothing = Nothing
the Functor
instance Maybe
corresponds to a functor from Hask
to Hask
in the following way:
To each concrete type
a
inHask
, we assign the concrete typeMaybe a
To each morphism
f :: A -> B
inHask
, we assign the morphismMaybe A -> Maybe B
which sendsNothing ↦ Nothing
andJust x ↦ Just (f x)
.
The Constant (endo)Functor
The constant (endo)functor on a category C is a functor Δc : C → C
mapping each object of the category C to a fixed object c∈C
and each morphism of C to the identity morphism id_c : c → c
for the fixed object.
The Const
Functor
Consider Data.Functor.Const
. For clarity, I will redefine it here, distinguishing between the type constructor Konst :: * -> * -> *
and the data constructor Const :: forall a,b. a -> Konst a b
.
newtype Konst a b = Const { getConst :: a }
instance Functor (Konst m) where
fmap :: (a -> b) -> Konst m a -> Konst m b
fmap _ (Const v) = Const v
This type checks because the data constructor Const
is polymorphic:
v :: a
(Const v) :: forall b. Konst a b
I can buy that Konst m
is an endofunctor in the category Hask
, since in the implmenetation of fmap
,
- on the left-hand side,
Const v
manifests itself as aKonst m a
, which is ok due to polymorphism - on the right-hand side,
Const v
manifests itself as aKonst m b
, which is ok due to polymorphism
But my understanding breaks down if we try to think of Konst m :: * -> *
as a constant functor in the category-theoretic sense.
What is the fixed object? The type constructor
Konst m
takes some concrete typea
and gives us aKonst m a
, which, at least superficially, is a different concrete type for everya
. We really want to map each typea
to the fixed typem
.According to the type signature,
fmap
takes anf :: a -> b
and gives us aKonst m a -> Konst m b
. IfKonst m
were analogous to the constant functor,fmap
would need to send every morphism to the identity morphismid :: m -> m
on the fixed typem
.
Questions
So, here are my questions:
In what way is Haskell's
Const
functor analogous to the constant functor from category theory, if at all?If the two notions are not equivalent, is it even possible to express the category-theoretic constant functor (call it
SimpleConst
, say) in Haskell code? I gave it a quick try and ran into the same problem with polymorphism wrt phantom types as above:
data SimpleKonst a = SimpleConst Int
instance Functor SimpleConst where
fmap :: (a -> b) -> SimpleConst a -> SimpleConst b
fmap _ (SimpleConst x) = (SimpleConst x)
If the answer to #2 is yes, If so, in what way are the two Haskell functions related in the category-theoretic sense? That is,
SimpleConst
is toConst
in Haskell as the constant functor is to__?__
in category theory?Do phantom types pose a problem for thinking of
Hask
like a category? Do we need to modify the definition ofHask
so that objects are really equivalence classes of types that would otherwise be identical if not for the presence of a phantom type parameter?
Edit: A Natural Isomorphism?
It looks like the polymorphic function getConst :: forall a,b. Konst a b -> a
is a candidate for a natural isomorphism η : (Konst m) ⇒ Δm
from the functor Konst m
to the constant functor Δm : Hask → Hask
, even though I haven't been able to establish yet whether the latter is expressible in Haskell code.
The natural transformation law would be η_x = (Konst m f) . η_y
. I'm having trouble proving it, since I'm not sure how to formally reason about the conversion of a (Const v)
from type Konst m a
to Konst m b
, other than handwaving that "a bijection exists!".
Related References
Here is a list of possibly related questions / references not already linked above:
- StackOverflow, "Do all Type Classes in Haskell have a Category-Theoretic Analogue?"
- StackOverflow, "How are functors in Haskell related to functors in category theory?"
- WikiBooks, Haskell/Category Theory
The problem we have here is that a functor is mathematically speaking a dependent pair, but it's a pair where one side (the
Type -> Type
mapping) lives in Haskell's type-level world, whereas the other side (the(a -> b) -> f a -> f b
mapping) lives in the value-level world. Haskell doesn't have a way to express such pairs. TheFunctor
class tricks its way around this limitation by allowing only type constructors as theType -> Type
mapping.The reason this helps is that type constructors are unique, i.e. every one of them can be assigned a well-defined morphism-mapping through Haskell's typeclass mechanism. But the flip side is that, well, the result is always unique, so you end up with the situation where
Konst Int Char
andKonst Int Bool
are technically speaking different, albeit isomorphic, types.A more mathematical way of expressing functors would require a separate means of identifying at the type level what functor you mean. Then you only need a type-level mapping (which can be done with type families) and a type→value-level mapping (typeclass):
This would still allow you to have also instances for the standard functors:
But it would be more awkward to use in practice. You'll notice that the
FunctorMphM
class requires-XAllowAmbiguousTypes
to compile – that's becausef
can't be inferred fromFunctorTyM f
. (We could ameliorate this with injective type families, but that would just get us back to the same issue we started with: the issue is precisely that the const functor is not injective!)With modern Haskell, that's ok though, it just means you need to be explicit about what functor you're working with. (Arguably, that would often be a good thing anyway!) Full example:
Output:
This approach has some other advantages too. For instance we can finally express that tuples are functors in each of their arguments, not just in the rightmost one: