TL;DR Scroll to the end and hit that REPL link.
Let's combine what we've already learned about Functors and Functions as a quick refresher:
// Functionally equivalent // def stringify(n: Int): String val stringify1: Int => String
=> operator is syntactic sugar around
Function1[A, B], therefore, this is also equivalent:
val stringify2: Function1[Int, String]
In Category Theory, this is considered a morphism. In the category of types, we can say we have an arrow from A to B. Now suppose we have two functors,
G[A]. A morphism, aka an arrow, from
G is possible in the category of functors. This can be written as follows:
val stringify3: FunctionK[F, G]
What this means is that we now have an arrow from F to G in the category of functors. This is called a natural transformation. The idea is to preserve the internal structure while providing a different context, or wrapper for your internal structure.
Please note that the
1 in the
Function1 above is referring to the arity of the function. It takes 1 argument. The
FunctionK represents that this typeclass effects higher-kinded types, which happen to be functors. Fortunately,
FunctionK has syntactic sugar similar to the function arrows.
val stringify4: F ~> G
We've been talking about functors in terms of F and G, so here's a couple concrete examples:
val stringify5: List ~> Option val stringify6: Option ~> Id
Id[_], and other similar functors preserve the inner shape. Although natural transformations are usually used in conjunction with
Free, they are not mutually exclusive.
Meet Scastie, an online REPL where you can try out what we spoke about here: https://scastie.scala-lang.org/animatedlew/iZflMSoXQEezn9KCk1ahKA/4