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

Commutative diagram for natural transformation

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

Tags:

Haskell