What is a natural transformation in haskell?
A natural transformation, without getting into the category theory behind it, is really just a polymorphic function.
Prelude> :set -XRankNTypes
Prelude> :set -XTypeOperators
Prelude> type (~>) f g = forall x. f x -> g x
The ~>
operator maps a type constructor to another type constructor, in such a way that it works for any given argument to the first type constructor. The TypeOperator
extension is what lets us use ~>
instead of a name like NatTrans
; the RankNTypes
is what lets us use forall
in the definition so that the caller can choose which type f
and g
will be applied to.
Here is an example of a natural transformation from Maybe
to List
, which takes a Maybe a
for any type a
, and produces an equivalent list (by returning either an empty list or the wrapped value as a singleton list).
Prelude> :{
Prelude| m2l :: Maybe ~> [] -- Maybe a -> [a]
Prelude| m2l Nothing = []
Prelude| m2l (Just x) = [x]
Prelude| :}
Prelude> m2l Nothing
[]
Prelude> m2l (Just 3)
[3]
Prelude> m2l (Just 'c')
"c"
An "inverse" would be l2m :: [] ~> Maybe
, with l2m [] = Nothing
and l2m (x:_) = Just x
. ( I put inverse in quotes because m2l (l2m [1,2,3]) /= [1,2,3]
)
Nothing prevents you from using IO
as either of the type constructors (although if IO
is on the left, it must be on the right as well).
foo :: IO ~> IO
foo a = putStrLn "hi" >> a
Then
> foo (putStrLn "foo")
hi
foo
> foo (return 3)
hi
3
Another example would be to view length
as a natural transformation from []
to Const Int
(adapted from https://bartoszmilewski.com/2015/04/07/natural-transformations/, which I highly recommend reading):
-- Isomorphic to builtin length, as Const Int is isomorphic to Int
-- Requires importing Data.Functor.Const
length' :: [] ~> Const Int
length' [] = Const 0
length' (x:xs) = Const (1 + getConst (length' xs))
It's useful to be explicit here: a natural transformation is a function of a signature
η :: ∀ a . Φ a -> Ψ a
where Φ
and Ψ
are functors. The ∀
is of course implied in Haskell, but that really is the crucial thing about natural transformations. That, and the commutative diagram
i.e. in Haskell,
(fmap f :: Ψ x -> Ψ y) . (η :: Φ x -> Ψ x)
≡ (η :: Φ y -> Ψ y) . (fmap f :: Φ x -> Φ y)
or short, fmap f . η ≡ η . fmap f
. (But note well that both η
have different type here!)
For example, I could transform:
Maybe a ~> List a
Mm, yyyea...ish. Again, be explicit what you mean. Concretely, the following is a natural transformation from Maybe
to []
(it is not a natural transformation from Maybe a
to [a]
, that wouldn't make sense):
maybeToList :: Maybe a -> [a]
maybeToList Nothing = []
maybeToList (Just a) = [a]
but it's not the only such transformation. In fact there are ℕ many such transformations, such as
maybeToList9 :: Maybe a -> [a]
maybeToList9 Nothing = []
maybeToList9 (Just a) = [a,a,a,a,a,a,a,a,a]
What about
IO
, it is impossible to do natural transformation right?
That's possible as well. For instance, here's a natural transformation from NonEmpty
to IO
:
import qualified Data.List.NonEmpty as NE
import System.Exit (die)
selectElem :: NonEmpty a -> IO a
selectElem l = do
let len = NE.length l
putStrLn $ "Which element? (Must be <"++show len++")"
i <- readLine
if i<len then die "Index too big!"
else return $ l NE.! i