Not another Monad Tutorial WiP
a word of caution
This is my second pass at this post and I’m using it to drive my understanding. I’m not confident everything is correct just yet. While the WiP tag is attached to this post, please take this information with a grain of salt.
A monad is just a monoid in the category of endofunctors, what’s the problem?
If you’re interested in Functional Programming (FP) you have probably stumbled across the above quote. When you google it, you will find multipart essays and heated discussions with claims that some are getting it all wrong and others are gate keeping. A lot has been made of this quote, but it doesn’t hurt to remember James Iry included it as a joke at how complex FP can be to new-comers. Let us try to demystify the quote so that it doesn’t stop you from discovering the powerful concepts inside FP.
First let us start with some definitions:
- monoid
- A binary function
combine: (A, A) -> A
that is associative and combines with an identity element which leaves the other element unchanged. - Example:
import java.util.stream.Stream
object Monoid {
val identity = ""
val combine: (String, String) -> String = { a, b -> a + b }
}
fun main() {
Monoid.apply {
println("""Monoid: combine("a", "b") = """ + combine("a", "b"))
println("- is associative: ")
println(""" - combine("a", combine("b", "c")) = """ + combine("a", combine("b", "c")))
println(""" - combine(combine("a", "b"), "c") = """ + combine(combine("a", "b"), "c"))
println("""- identity does not change value: combine("a", identity) = """ + combine("a", identity))
println("""- safely combines in parallel: Stream.of("a", "b", "c", "d", "e").parallel().reduce(identity, combine) = """ +
Stream.of("a", "b", "c", "d", "e").parallel().reduce(identity, combine))
}
}
- endofunctor
- An endofunctor is a container type with a
map
function that applies a transformation to the contained value.map
returns the Functor with the transformed value and can be chained -
note: endofunctors are a special type of functor which
map
to the same category. In most cases: the category is all kotlin types. - Example:
data class Functor(val value: String) {
fun map(transform: (String) -> String) = Functor(transform(value))
}
val add_b: (String) -> String = {it + "b"}
val add_c: (String) -> String = {it + "c"}
fun main() {
println("""Intialize: Functor("a") = """ + Functor("a"))
println("""Map: Functor("a").map(add_b) = """ + Functor("a").map(add_b))
println("""Chain map: Functor("a").map(add_b).map(add_c) = """ + Functor("a").map(add_b).map(add_c))
}
- monad
- So… if a monad is a monoid which works on the category of endofunctors you get the following:
import java.util.stream.Stream
data class Functor(val value: String) {
object Monoid {
val identity = Functor("")
val combine: (Functor, Functor) -> Functor = { a, b -> a.map { it + b.value } }
}
fun map(transform: (String) -> String) = Functor(transform(value))
}
fun main() {
val a = Functor("a")
val b = Functor("b")
val c = Functor("c")
Functor.Monoid.apply {
println("""FunctorMonoid: combine(a, b) = """ + combine(a, b))
println("- is associative: ")
println(""" - combine(a, combine(b, c)) = """ + combine(a, combine(b, c)))
println(""" - combine(combine(a, b), c))= """ + combine(combine(a, b), c))
println("""- identity does not change value: combine(a, identity)) = """ + combine(a, identity))
println("""- safely combines in parallel: Stream.of("a", "b", "c", "d", "e").map{Functor(it)}.parallel().reduce(identity, combine)) = """ +
Stream.of("a", "b", "c", "d", "e").map{Functor(it)}.parallel().reduce(identity, combine))
}
}
Finally, we can define the Monad using map
from the functor and flatMap
using compose:
data class Monad(val value: String) {
object Monoid {
val identity = Monad("")
val combine: (Monad, Monad) -> Monad = { a, b -> a.map { it + b.value } }
}
fun map(transform: (String) -> String) = Monad(transform(value))
fun flatMap(transform: (String) -> Monad) = Monoid.combine(Monoid.identity, transform(value))
}
val add_b: (String) -> String = {it + "b"}
val add_c: (String) -> String = {it + "c"}
fun toMonad(t: (String) -> String) = { value: String -> Monad(t(value)) }
fun main() {
val a = Monad("a")
println("""Monad: a.flatMap(toMonad(add_b)) = """ + a.flatMap(toMonad(add_b)))
println("- is associative: ")
println(
""" - a.flatMap(toMonad(add_b)).flatMap(toMonad(add_c))= """
+ a.flatMap(toMonad(add_b)).flatMap(toMonad(add_c)) )
println(
""" - a.flatMap{ toMonad(add_b)(it).flatMap(toMonad(add_c)) } = """
+ a.flatMap{ toMonad(add_b)(it).flatMap(toMonad(add_c))} )
}