WSO2CON 2024 - Building the API First Enterprise – Running an API Program, fr...
Monads - Dublin Scala meetup
1. Monads
An introduction to the brave new world
…or the old one?
Mikhail Girkin
Software engineer, Wonga.com
2. 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
3. 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
4. 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],
…
)
5. 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)
6. … 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(…)
7. 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)
9. An unexpected move towards for
def outboundFlights(cities: Seq[City]): Seq[String] =
for {
city <- cities
airport <- city.airports
flight <- airport.outboundFlights
} yield flight.code
10. 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
12. 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]
13. 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
14. 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)
15. 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!
19. 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)
20. 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…
21. 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
22. 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!
23. 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!
24. 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
25. 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
26. 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!
27. 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
28. 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
29. 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
30. 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())
}
}
}
31. 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)
}
}
32. 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
}
33. 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
34. 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
35. 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
}
}
37. 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
38. 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
39. 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 => ...
...
}
40. 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
41. 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
42. 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)