3. WHY THE FREE MONAD ISN’T FREE
“Let’s just trampoline it and
add the Free Monad”
@kelleyrobinson
4. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
“Let’s just trampoline it and
add the Free Monad”
5.
6. Why The Free
Monad Isn’t Free
Kelley Robinson
Data & Infrastructure Engineer
Sharethrough
@kelleyrobinson
7. WHY THE FREE MONAD ISN’T FREE
- Monoids, Functors & Monads
- How to be “Free”
- Why & Why Not “Free”
- Alternatives
- Real World Applications
$
@kelleyrobinson
8. WHY THE FREE MONAD ISN’T FREE
github.com/robinske/monad-examples
9. WHY THE FREE MONAD ISN’T FREE
https://twitter.com/rickasaurus/status/705134684427128833
10. WHY THE FREE MONAD ISN’T FREE
Monoids
@kelleyrobinson
17. @kelleyrobinson
object FunctionComposition /* extends Monoid[_=>_] */ {
def append[A, B, C](f1: A => B, f2: B => C): A => C =
(a: A) => f2(f1(a))
def identity[A]: A => A = (a: A) => a
}
18. @kelleyrobinson
object FunctionComposition /* extends Monoid[_=>_] */ {
def append[A, B, C](f1: A => B, f2: B => C): A => C =
(a: A) => f2(f1(a))
def identity[A]: A => A = (a: A) => a
}
19. WHY THE FREE MONAD ISN’T FREE
Functors
@kelleyrobinson
21. WHY THE FREE MONAD ISN'T FREE
@kelleyrobinson
Properties
Identity: "no-op" value
Composition:
grouping doesn't matter
22. @kelleyrobinson
sealed trait Option[+A]
case class Some[A](a: A) extends Option[A]
case object None extends Option[Nothing]
object OptionFunctor extends Functor[Option] {
def map[A, B](a: Option[A])(fn: A => B): Option[B] =
a match {
case Some(something) => Some(fn(something))
case None => None
}
}
24. @kelleyrobinson
it("should follow the composition law") {
val f: String => String = s => s + "a"
val g: String => String = s => s + "l"
val h: String => String = s => s + "a"
assert(
map(Some("sc"))(f andThen g andThen h) ==
map(map(map(Some("sc"))(f))(g))(h) ==
"scala"
)
}
26. WHY THE FREE MONAD ISN’T FREE
Monads
@kelleyrobinson
27. "The term monad is a bit
vacuous if you are not a
mathematician. An alternative
term is computation builder."
WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson http://stackoverflow.com/questions/44965/what-is-a-monad
40. WHY THE FREE MONAD ISN’T FREE
- Monoids, Functors & Monads
- How to be “Free”
- Why & Why Not “Free”
- Alternatives
- Real World Applications
$
@kelleyrobinson
41. WHY THE FREE MONAD ISN'T FREE
The word "free" is
used in the sense of
"unrestricted" rather
than "zero-cost"
$
@kelleyrobinson
42. WHY THE FREE MONAD ISN'T FREE
"Freedom not beer"
https://en.wikipedia.org/wiki/Gratis_versus_libre#/media/File:Galuel_RMS_-_free_as_free_speech,_not_as_free_beer.png
43. WHY THE FREE MONAD ISN’T FREE
Free Monoids
@kelleyrobinson
45. WHY THE FREE MONAD ISN’T FREE
Free Monoids
• Free from interpretation
• No lost input data when
appending
@kelleyrobinson
image credit: http://celestemorris.com
46. @kelleyrobinson
// I'm free!
class ListConcat[A] extends Monoid[List[A]] {
def append(a: List[A], b: List[A]): List[A] =
a ++ b
def identity: List[A] = List.empty[A]
}
47. @kelleyrobinson
// I'm not free :(
object IntegerAddition extends Monoid[Int] {
def append(a: Int, b: Int): Int = a + b
def identity: Int = 0
}
48. WHY THE FREE MONAD ISN’T FREE
Free Monads
@kelleyrobinson
49. Don't lose any data!
(that means no evaluating functions)
WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
50. @kelleyrobinson
def notFreeAppend[A, B, C]
(f1: A => M[B], f2: B => M[C]): A => M[C] = {
a: A =>
// evaluate f1
val bs: M[B] = f1(a)
// evaluate f2
val cs: M[C] = flatMap(bs) { b: B => f2(b) }
cs
}
53. @kelleyrobinson
sealed trait Free[F[_], A] { self =>
}
case class Return[F[_], A](given: A) extends Free[F, A]
case class Suspend[F[_], A](fn: F[A]) extends Free[F, A]
54. @kelleyrobinson
sealed trait Free[F[_], A] { self =>
}
case class Return[F[_], A](given: A) extends Free[F, A]
case class Suspend[F[_], A](fn: F[A]) extends Free[F, A]
case class FlatMap[F[_], A, B]
(free: Free[F, A], fn: A => Free[F, B])
extends Free[F, B]
55. @kelleyrobinson
sealed trait Free[F[_], A] { self =>
def flatMap ...
def pure ...
def map ...
}
case class Return[F[_], A](given: A) extends Free[F, A]
case class Suspend[F[_], A](fn: F[A]) extends Free[F, A]
case class FlatMap[F[_], A, B]
(free: Free[F, A], fn: A => Free[F, B])
extends Free[F, B]
56. @kelleyrobinson
sealed trait Todo[A]
case class NewTask[A](task: A) extends Todo[A]
case class CompleteTask[A](task: A) extends Todo[A]
case class GetTasks[A](default: A) extends Todo[A]
def newTask[A](task: A): Free[Todo, A] =
Suspend(NewTask(task))
def completeTask[A](task: A): Free[Todo, A] =
Suspend(CompleteTask(task))
def getTasks[A](default: A): Free[Todo, A] =
Suspend(GetTasks(default))
57. @kelleyrobinson
val todos: Free[Todo, Map[String, Boolean]] =
for {
_ <- newTask("Go to scala days")
_ <- newTask("Write a novel")
_ <- newTask("Meet Tina Fey")
_ <- completeTask("Go to scala days")
tsks <- getTasks(Map.empty)
} yield tsks
58. @kelleyrobinson
val todosExpanded: Free[Todo, Map[String, Boolean]] =
FlatMap(
Suspend(NewTask("Go to scala days")), (a: String) =>
FlatMap(
Suspend(NewTask("Write a novel")), (b: String) =>
FlatMap(
Suspend(NewTask("Meet Tina Fey")), (c: String) =>
FlatMap(
Suspend(CompleteTask("Go to scala days")),
(d: String) =>
Suspend(GetTasks(default = Map.empty))
)
)
)
)
59. WHY THE FREE MONAD ISN’T FREE
- Monoids, Functors & Monads
- How to be “Free”
- Why & Why Not “Free”
- Alternatives
- Real World Applications
$
@kelleyrobinson
60. WHY THE FREE MONAD ISN'T FREE
What's the point?
• Defer side effects
• Multiple interpreters
• Stack safety
@kelleyrobinson
68. @kelleyrobinson
def runFree[F[_], G[_], A]
(f: Free[F, A])
(transform: FunctorTransformer[F, G])
(implicit G: Monad[G]): G[A]
Turn F into G -
AKA "Natural Transformation"Input
`G` must be a monad so we can flatMap
70. @kelleyrobinson
/* Function body */
@annotation.tailrec
def tailThis(free: Free[F, A]): Free[F, A] = free match {
case FlatMap(FlatMap(fr, fn1), fn2) => ...
case FlatMap(Return(a), fn) => ...
case _ => ...
}
tailThis(f) match {
case Return(a) => ...
case Suspend(fa) => ...
case FlatMap(Suspend(fa), fn) => ...
case _ => ...
}
https://github.com/robinske/monad-examples
71. @kelleyrobinson
tailThis(f) match {
case Return(a) => ...
case Suspend(fa) => transform(fa)
case FlatMap(Suspend(fa), fn) =>
monad.flatMap(transform(fa))(a =>
runFree(fn(a))(transform))
case _ => ...
}
https://github.com/robinske/monad-examples
72. @kelleyrobinson
def runLoop[F[_], G[_], A](...): G[A] = {
var eval: Free[F, A] = f
while (true) {
eval match {
case Return(a) => ...
case Suspend(fa) => ...
case FlatMap(Suspend(fa), fn) => ...
case FlatMap(FlatMap(given, fn1), fn2) => ...
case FlatMap(Return(s), fn) => ...
}
}
throw new AssertionError("Unreachable")
}
73. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
Evaluating
Applies transformation on
`Suspend`
Trampolining for stack safety
75. @kelleyrobinson
type Id[A] = A
case class TestEvaluator(var model: Map[String, Boolean])
extends FunctorTransformer[Todo, Id] {
def apply[A](a: Todo[A]): Id[A]
}
76. @kelleyrobinson
a match {
case NewTask(task) =>
model = model + (task.toString -> false)
task
case CompleteTask(task) =>
model = model + (task.toString -> true)
task
case GetTasks(default) =>
model.asInstanceOf[A]
}
77. @kelleyrobinson
it("should evaluate todos") {
val result =
runFree(todos)(TestEvaluator(Map.empty))
val expected: Map[String, Boolean] =
Map(
"Go to scala days" -> true,
"Write a novel" -> false,
"Meet Tina Fey" -> false
)
result shouldBe expected
}
79. @kelleyrobinson
a match {
case NewTask(task) =>
actions = actions :+ NewTask(task.toString)
task
case CompleteTask(task) =>
actions = actions :+ CompleteTask(task.toString)
task
case GetTasks(default) =>
actions = actions :+ GetTasks("")
default
}
80. @kelleyrobinson
it("should evaluate todos actions in order") {
runFree(todos)(ActionTestEvaluator)
val expected: List[Todo[String]] =
List(
NewTask("Go to scala days"),
NewTask("Write a novel"),
NewTask("Meet Tina Fey"),
CompleteTask("Go to scala days"),
GetTasks("")
)
ActionTestEvaluator.actions shouldBe expected
}
81. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
Defining multiple interpreters allows
you to test side-effecting code without
using testing mocks.
82. @kelleyrobinson
// Production Interpreter
def apply[A](a: Todo[A]): Option[A] = {
a match {
case NewTask(task) =>
/**
* Some if DB write succeeds
* None if DB write fails
*
*/
case CompleteTask(task) => ...
case GetTasks(default) => ...
}
}
83. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
Justifications
• Defer side effects
• Multiple interpreters
• Stack safety
84. WHY THE FREE MONAD ISN'T FREE
#BlueSkyScala
The path to learning is broken
@kelleyrobinson
Credit: Jessica Kerr
85. WHY THE FREE MONAD ISN'T FREE
Freedom isn't free
Reasons to avoid the Free Monad
• Boilerplate
• Learning curve
• Alternatives
@kelleyrobinson
Credit: Jessica Kerr
86. WHY THE FREE MONAD ISN’T FREE
- Monoids, Functors & Monads
- How to be “Free”
- Why & Why Not “Free”
- Alternatives
- Real World Applications
$
@kelleyrobinson
87. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
Know your domain
88. WHY THE FREE MONAD ISN'T FREE
Functional Spectrum
Where does your team fall?
Java Haskell
89. WHY THE FREE MONAD ISN'T FREE
Functional Spectrum
Where does your team fall?
Java Haskell
90. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
Alternatives for
maintaining stack
safety
91. @kelleyrobinson
final override def map[B, That](f: A => B)
(implicit bf: CanBuildFrom[List[A], B, That]): That = {
if (bf eq List.ReusableCBF) {
if (this eq Nil) Nil.asInstanceOf[That] else {
val h = new ::[B](f(head), Nil)
var t: ::[B] = h
var rest = tail
while (rest ne Nil) {
val nx = new ::(f(rest.head), Nil)
t.tl = nx
t = nx
rest = rest.tail
}
h.asInstanceOf[That]
}
}
else super.map(f)
}
92. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
Alternatives for
managing side
effects
94. @kelleyrobinson
def handleFailure[A](f: => A): ActionResult / A = {
Try(f) match {
case Success(res) => res.right
case Failure(e) =>
InternalServerError(reason = e.getMessage).left
}
}
handleFailure(getPerson(rs))
95. WHY THE FREE MONAD ISN’T FREE
- Monoids, Functors & Monads
- How to be “Free”
- Why & Why Not “Free”
- Alternatives
- Real World Applications
$
@kelleyrobinson
96. WHY THE FREE MONAD ISN'T FREE
Scalaz
Scalaz is a Scala library for functional programming.
http://scalaz.github.io/scalaz/
Cats
Lightweight, modular, and extensible library for
functional programming.
http://typelevel.org/cats/
@kelleyrobinson
http://www.slideshare.net/jamesskillsmatter/real-world-scalaz
98. @kelleyrobinson
import scalaz.concurrent.Task
def apply(conf: Config, messages: List[SQSMessage]): Unit = {
val tasks = messages.map(m => Task {
processSQSMessage(conf, m)
})
Task.gatherUnordered(tasks).attemptRun match {
case -/(exp) => error(s"Unable to process message")
case _ => ()
}
}
99. @kelleyrobinson
// yikes
object Task {
implicit val taskInstance:
Nondeterminism[Task] with Catchable[Task]
with MonadError[({type λ[α,β] =
Task[β]})#λ,Throwable] = new
Nondeterminism[Task] with Catchable[Task]
with MonadError[({type λ[α,β] =
Task[β]})#λ,Throwable] { ... }
}
100. WHY THE FREE MONAD ISN’T FREE
@kelleyrobinson
My experience...
...what happened?
101. WHY THE FREE MONAD ISN’T FREE
- Know your domain
- Use clean abstractions
- Share knowledge
$
@kelleyrobinson