https://bartoszmilewski.com/2015/04/07/natural-transformations/
- consider two functors
F
andG
between categoriesC
andD
- a natural transformation is a selection of morphisms:
for every object
a
, it picks one morphism fromF a
toG a
. If we call the natural transformationα,
this morphism is called the component ofα
ata
, orαa
. αa :: F a -> G a
a
is an object inC
whileαa
is a morphism inD
- if, for some
a
, there is no morphism betweenF a
andG a
inD
, there can be no natural transformation betweenF
andG
- naturality condition:
G f ∘ αa = αb ∘ F f
- we can think about functors as generalized containers
- we can consider natural transformations to be recipes for repackaging the contents of one container into another and we are not touching the items themselves
- naturality condition: it doesn’t matter whether we modify the items first (fmap), and repackage later; or repackage first, and then modify the items in the new container (with new fmap)
- existence of natural transformations between two functors is far from guaranteed
- it maps objects to morphisms
- it maps morphisms to commuting squares — there is one commuting naturality square in
D
for every morphism inC
(naturality condition)
αa :: F a -> G a
- pseudo-haskell:
alpha_a :: F a -> G a
- pseudo-scala:
def alpha_a[A](F: F[A]): G[A]
- naturality condition:
mapG f . alpha_a = alpha_b . mapF f
- a parametrically polymorphic function between two functors is always a natural transformation
We will provide easy examples of polymorphic functions (natural transformations) in scala
- list -> option (some(head) or none)
- haskell
safeHead :: [a] -> Maybe a safeHead [] = Nothing safeHead (x:xs) = Just x
- scala
note that list has build-in exactly that method:
def safeHead[A](list: List[A]): Option[A] = list match { case Nil => None case x :: xs => Some(x) }
list.headOption
- haskell
- option -> list (one elem or empty)
note that option has build-in exactly that method:
def toList[A](option: Option[A]): List[A] = option match { case None => Nil case Some(x) => List(x) }
option.toList
- list a -> int (const int a): length
- Const: https://github.com/mtumilowicz/scala212-category-theory-const-functor
- haskell
length :: [a] -> Const Int a length [] = Const 0 length (x:xs) = Const (1 + unConst (length xs)) unConst :: Const c a -> c unConst (Const x) = x
- scala
obviously:
def lengthOf[A](list: List[A]): Const[Int, A] = list match { case Nil => Const(0) case x :: xs => Const(1 + lengthOf(xs).param) }
list.length
is much easier
- reader () a -> option a
- for every type e, you can define a family of natural transformations from
Reader e
to any other functorf
Reader ()
takes any typea
and maps it into a function type()->a
- all the functions that pick a single element from the seta
- we could think about
Reader ()
as a supplier - haskell
alpha :: Reader () a -> Maybe a
- only two possible implementations:
dumb (Reader _) = Nothing
obvious (Reader g) = Just (g ())
- scala
object ReaderOptionNaturalTransformation { def trivial[A](reader: Reader[Unit, A]): Option[A] = None def obvious[A](reader: Reader[Unit, A]): Option[A] = Some(reader()) }
- for every type e, you can define a family of natural transformations from
- reader () a -> list a: trivial, obvious (composition of defined above natural transformations)
def trivial[A](reader: Reader[Unit, A]): List[A] = { val toList = (option: Option[A]) => OptionListNaturalTransformation.toList(option) val trivial = (reader: Reader[Unit, A]) => ReaderOptionNaturalTransformation.trivial(reader) toList.compose(trivial).apply(reader) } def obvious[A](reader: Reader[Unit, A]): List[A] = { val toList = (option: Option[A]) => OptionListNaturalTransformation.toList(option) val trivial = (reader: Reader[Unit, A]) => ReaderOptionNaturalTransformation.obvious(reader) toList.compose(trivial).apply(reader) }
- reader boolean a -> option a: three transformations
def trivial[A](reader: Reader[Boolean, A]): Option[A] = None def truth[A](reader: Reader[Boolean, A]): Option[A] = Some(reader(true)) def untruth[A](reader: Reader[Boolean, A]): Option[A] = Some(reader(false))
NaturalTransformationsTest
- list -> option: safe head
ListOptionNaturalTransformation.safeHead(List(1)) should be(Some(1)) ListOptionNaturalTransformation.safeHead(List()) should be(None)
- option -> list
OptionListNaturalTransformation.toList(None) should be (List()) OptionListNaturalTransformation.toList(Some(1)) should be (List(1))
- reader () a -> option a: trivial, obvious
def reader: Reader[Unit, String] = _ => "a" ReaderOptionNaturalTransformation.trivial(reader) should be(None) ReaderOptionNaturalTransformation.obvious(reader) should be(Some("a"))
- reader () a -> list a: trivial, obvious (composition of defined above natural transformations)
def trivial[A](reader: Reader[Unit, A]): List[A] = OptionListNaturalTransformation.toList[A].compose(ReaderOptionNaturalTransformation.trivial[A]).apply(reader) def obvious[A](reader: Reader[Unit, A]): List[A] = OptionListNaturalTransformation.toList[A].compose(ReaderOptionNaturalTransformation.obvious[A]).apply(reader)
- reader boolean a -> option a: three transformations
ReaderBooleanToOption.trivial(reader) should be (None) ReaderBooleanToOption.truth(reader) should be (Some("true")) ReaderBooleanToOption.untruth(reader) should be (Some("false"))
- list a -> const int a: length
ListLengthAsNaturalTransformation.lengthOf(List()) should be(Const(0)) ListLengthAsNaturalTransformation.lengthOf(List(1, 2, 3, 4)) should be(Const(4))