Monads An introduction to the brave new world …or the old one? Mikhail Girkin Software engineer, Wonga.com
Monads An introduction to the brave new world
…or the old one?
Mikhail Girkin
Software engineer, Wonga.com
About me
About 8 years of development experience
PhD in engineering
About 4 years worked as university professor
Mainly C# developer, mainly back-end
Interested in functional and hybrid languages
Doing Scala in evening-projects
Agenda
C# devs, Java 8 devs already use monads, they just don’t know about it
A simple definition of monad
Scala for-comprehension and monads
Monads for everyday usage Seq
Option (Maybe)
Try
Future
Random distrubution monad
An example of “could-be-real” code
Q&A
You’ve already used it
case class Flight(code: String, …) case class Airport( outboundFlights: Seq[Flight], inboundFlights: Seq[Flight], … ) case class City( airports: Seq[Airport], … )
You’ve already used it
We have a set of cities as an input, and want to get all the outbound flight codes from them:
def outboundFlights(cities: Seq[City]): Seq[String] =
cities.flatMap(c => c.airports)
.flatMap(airp => airp.outboundFlights)
.map(flight => flight.code)
… and even in C# or Java
Cities
.SelectMany(c => c.Airports)
.SelectMany(a => a.OutboundFlights)
.Select(f => f.FlightCode)
cities
.flatMap(c -> c.getAirports().stream())
.flatMap(a -> a.getOutboundFlights().stream())
.map(f -> f.getFlightCode())
.collect(…)
If you don’t know about flatMap
Collection API from functional world
Easy and expressive way to deal with collections
Map map[T, TOut](Seq[T], T => TOut): Seq[TOut] Seq[T] => (T => TOut) => Seq[TOut] Seq(1, 2, 3, 4).map(x => x+1) == Seq(2, 3, 4, 5)
flatMap flatMap[T, TOut](Seq[T], T => Seq[TOut]): Seq[TOut] Seq[T] => (T => Seq[TOut] => Seq[TOut] Seq(1, 2, 3, 4).flatMap(x => Seq(x, x+1)) == Seq(1, 2, 2, 3, 3, 4, 4, 5)
An unexpected move
def outboundFlights(cities: Seq[City]): Seq[String] =
cities
.flatMap(c => c.airports
.flatMap(airp => airp.outboundFlights
.map(flight => flight.code)
)
)
Bulky and cumbersome?
An unexpected move towards for
def outboundFlights(cities: Seq[City]): Seq[String] =
for {
city <- cities
airport <- city.airports
flight <- airport.outboundFlights
} yield flight.code
A little bit more for comprehensions
//Outbound flights from international airports
def outInternationalFlights(cities: Seq[City]): Seq[String] =
for {
city <- cities
airport <- city.airports if airport.isInternational
flight <- airport.outboundFlights
} yield flight.code
withFilter!
withFilter: Seq[T] => (T => Boolean) => Seq[T]
def outboundFlights(cities: Seq[City]): Seq[String] =
cities
.flatMap(c =>
c.airports
.withFilter(a => a.isInternational)
.flatMap(airp => airp.outboundFlights.map(
flight => flight.code
))
)
A magic of flatMap
1. cities.flatMap(c => c.airports)
2. flatMap(cities, c => airports)
3. flatMap(Seq[City], City => Seq[Airports]): Seq[Airports]
4. flatMap[A, B](Seq[A], A => Seq[B]): Seq[B]
5. Seq[A] => (A => Seq[B]) => Seq[B]
And some magic:
M[A] => (A => M[B]) => M[B]
Welcome the monad!
Nothing more then a design pattern
Consist of 3 main parts: Type: A
Unit (construction) operation: A => M[A]
Bind (SelectMany, flatMap) operation: M[A] => (A => M[B]) => M[B]
Other common operations: Map: M[A] => (A => B) => M[B]
Lift: (A => B) => (M[A] => M[B])
Encapsulates some computation with the defined rules Seq is a monad. Rules of computation – apply the given function to each of the value,
combine the results in a Seq
Or some other definitions
1. Monads are return types that guide you through the happy path. (Erik Meijer)
2. Monads are parametric types with two operations flatMap and unit that obey some algebraic laws. (Martin Odersky)
3. Monads are structures that represent computations defined as sequences of steps. (Wikipedia)
4. Monads are chainable container types that trap values or computations and allow them to be transformed in confinement. (@mttkay)
Laws of monads
Let’s say unit(x) – monad constructor, m – monad object, f – function A => M[A]
1. unit(x).flatMap(f) == f(x)
2. m.flatMap(x => unit(x)) == m
3. m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))
Forget about laws, let mathematicians earn their money, let’s do something usefull!
Maybe?
Let’s imagine Java-style (C#-style) programming in Scala
def getPaymentCardId(accountId: UUID): UUID = {
val account = accountDao.get(accountId)
if(account == null) return null
if(account.paymentCard == null) return null
return account.paymentCard.paymentCardId
}
Maybe… Option!
trait Option[T] {
def flatMap[TOut](f: T => Option[TOut]): Option[TOut]
def map[TOut](f: T => TOut): Option[TOut]
}
case class Some[T] (value: T) extends Option[T] {
def flatMap[TOut](f: T => Option[TOut]) = f(value)
def map[TOut](f: T => TOut): Option[TOut] = ??? /* Exercise! */
}
case object None extends Option[Nothing] {
def flatMap[T, TOut](f: T => Option[TOut]) = None
def map[TOut](f: T => TOut): Option[TOut] = ??? /* Exercise! */
}
Maybe!
def getPaymentCardId(accountId: UUID) =
accountDao.get(accountId)
.flatMap(acc => acc.paymentCard)
.map(card => card.paymentCardId)
def getPaymentCardId(accountId: UUID) =
for {
account <- accountDao.get(accountId)
card <- account.paymentCard
} yield card.paymentCardId
Or even maybe... remember flatMap!
val firstNameOpt: Option[String]
val lastNameOpt: Option[String]
case class Person(
firstName: String,
lastName: String
)
for {
firstName <- firstNameOpt
lastName <- lastNameOpt
} yield Person(firstName, lastName)
What do we get?
Code is readable
Concentration on happy path, non-happy path is delegated to monad
Code is maintainable
Less errors
No NullReferenceException! Never!
Using for-loops for monads is mind-blowing feature for a developer with an imperative background
But once you get used to it…
Exceptions, exceptions, exceptions…
Every Java or C# developer sometimes ends up with something like this:
try
{
SomeType value = connection.getSomething()
return connection.getAnotherSomething(value)
}
catch (Exception exc)
{
// do other stuff
}
It is bulky and cumbersome, as well as time-consuming for stack rewind
Exceptions… try, not catch!
trait Try[T]
case class Success[T](value: T) extends Try[T]
trait Failure extends Try[Nothing]
case class NotFound(exc: NotFoundException) extends Failure
case class NotAuthorized(exc: SecurityException) extends Failure
…
flatMap and map are easy to implement!
flatMap for Try
Start with the type: Try[T] => (T => Try[TOut]) => Try[TOut] def flatMap[T, TOut](in: Try[T], action: T => Try[TOut]): Try[TOut] = { in match { case Success(value) => action(value) case _ => in } } Try is a monad!
Try!
val result = for {
value <- connection.getSomething()
otherValue <- connection.getAnotherSomething(value)
} yield otherValue
result match {
case Success(value) => …
case NotFound => …
case NotAuthorized => …
}
Returns Try[T]
Type of value explicitly says that the client should deal with exceptions
The code is clean as concise
Exceptions is not an only case for Try-Failure monad. The monad called “Error” in generic case, example is coming
Asynchronously blowing up mind
Writing async code is another level of complexity
In pure C# 2 (I suppose Java too) async code often ended up with spaghetti-like code
We are going to live in a non-blocking world
Bright future
A Future is an object holding a value which may become available at some point.
Resolves in two possible ways When a Future is completed with a value, we say that the future was
successfully completed with that value.
When there has been an error, we say that the future completed with failure
What is the difference with Error monad? Async!
So someone will care about async!
The magic of Future
price: Future[Money] = nasdaq.getSharePrice(“AAPL”)
Is value there? – Don’t know
Could we do something with it? – Why not?
price.map(p => costOfOurStock(p)) //returns Future
price.flatMap(
p => nasdaq.bid(“AAPL”, p-0.01, 10)) //Future on Future
Future
Will not go deep into Promises-Futures
Future is a good example, where flatMap shines
flatMap abstracts everything from the developer except the operations on values
Concentration on happy path at the most extent
Random distribution monad
A value inside a monad is some random value of type T distributed according to some law
A good an uncommon example of monad application
trait Distribution[T] {
def sample(): T
def flatMap[TOut](f: T => Distribution[TOut]): Distribution[TOut]
def map[TOut](f: T => TOut): Distribution[TOut]
}
Each time sample is called new random value is generated
Source: https://github.com/mikegirkin/randommonad
Implementation
trait Distribution[T] {
self =>
def sample(): T
def flatMap[TOut](f: T => Distribution[TOut]): Distribution[TOut] = {
new Distribution[TOut] {
override def sample = f(self.sample()).sample()
}
}
def map[TOut](f: T => TOut): Distribution[TOut] = {
new Distribution[TOut] {
override def sample = f(self.sample())
}
}
}
A first real implementation
A uniform distribution of discrete random values from the given alphabet
def uniform[T](values: Array[T]): Distribution[T] = new Distribution[T] {
override def sample: T = {
val index = rnd.nextInt(values.size)
values(index)
}
}
Extracting distribution
Results in sequence of (value, probability)
def histo: Seq[(T, Double)] = {
val n = 1000000
val map = scala.collection.mutable.Map[T, Double]()
for(i <- 1 to n) {
val s = sample()
if(map.isDefinedAt(s)) map(s) = map(s) + 1.0/n
else map(s) = 1.0/n
}
map.toSeq
}
What could we do?
Ever played D&D game?
def dice(sides: Int) =
Distribution.uniform(Range(1, sides+1, 1))
val d6 = dice(6)
val d20 = dice(20)
val d3d6 = for {
x <- d6
y <- d6
z <- d6
} yield x+y+z
18: 0.0046 17: 0.0139 16: 0.0278 15: 0.0464 14: 0.0692 13: 0.0972 12: 0.1156 11: 0.1251 10: 0.1255 9: 0.1161 8: 0.0970 7: 0.0694 6: 0.0463 5: 0.0274 4: 0.0140 3: 0.0047
Drivers and accidents
Say we have a crossroads with the yellow traffic lights at the moment, and to drivers approaching from the orthogonal directions.
Drivers:
0.2 – aggressive, will accelerate with probability 0.9
0.6 – normal, will accelerate with probability 0.2
0.2 – cautious, will accelerate with probability 0.1
Let’s use the random monad
Given distribution
def given[T](probs: Seq[(Double, T)]): Distribution[T] =
new Distribution[T] {
//result in i.e. ((0.2 -> cautious), (0.8 -> normal), (1.0 -> aggressive))
val aggrProbs = probs
.scanLeft(0.0)((p, x) => p + x._1)
.drop(1)
.zip(probs.map(_._2))
override def sample: T = {
val r = rnd.nextDouble()
aggrProbs.find(x => x._1 >= r).get._2
}
}
Keep code simple
def aggressive = Distribution.given(Seq((0.9 -> true), (0.1 -> false)))
def normal = Distribution.given(Seq((0.2 -> true), (0.8 -> false)))
def cautious = Distribution.given(Seq((0.1 -> true), (0.9 -> false)))
def driver = Distribution.given(Seq(
(0.2 -> cautious), (0.6 -> normal), (0.2 -> aggressive)
))
val collision = for {
d1 <- driver
d2 <- driver
act1 <- d1
act2 <- d2
} yield act1 && act2
Results in: false: 0.8976 true: 0.1024
A real code (still simplified) example
Let’s say we want to implement user updating the ticket in the ticket-tracking system
A usecase (happy path): user fills the form with the updated values, and presses “Update”. The system updates the ticket in the storage, and redirects to the ticket information page, and then sends the email notification.
What can go wrong?
Invalid values submitted
There is no ticket to update
The user is not allowed update that ticket
The db could fail to perform the update
Monad!
Error monad is what we want!
trait MyAppResult[T]
case class MyAppSuccess[T](result: T) extends MyAppResult[T]
trait MyAppFailure extends MyAppResult[Nothing]
case class InvalidInput(
errors: Seq[InputError]) extends MyAppFailure
case object TicketNotFound extends MyAppFailure
case object NoPermission extends MyAppFailure
case object DbFailure extends MyAppFailure
The code sketch val form = getUserInput()
for {
validatedInput <- validateForm(form)
ticket <- retreiveTicket(validatedInput.ticketId)
_ <- check(currentUser.canEditTicket(ticket))
updatedTicket <- updateTicket(ticket, validatedInput)
_ <- saveTicket(updatedTicket)
} yield updatedTicket
updatedTicket match {
case MyAppSuccess(ticket) => ...
case InvalidInput(errors) => ...
case TicketNotFound => ...
...
}
Recap: Monadic pattern
1. Get initial value enclosed in monad
2. Do something with the value enclosed, get monad
3. Do something with the value enclosed, get monad
4. …
5. PROFIT!!! Result resolution
Recap
Monads is the way to execute some computation following the common rules
Monads is all about flatMap, nothing more
Monads is the way to abstract common rules and reuse them
It is not a rocket science, and you’ve already used them
Monads for everyday use: Seq Option Try/Error Future
Some links
Erik Meijer - Contravariance is the Dual of Covariance (http://www.infoq.com/interviews/meijer-monads)
Principles of reactive programming (https://www.coursera.org/course/reactive)
Robert Martin - Monads within Clojure (https://www.youtube.com/watch?v=Usxf3aLimtU)
Dick Wall - What Have The Monads Ever Done For Us (https://www.youtube.com/watch?v=2IYNPUp751g)