Top Banner
Monads An introduction to the brave new world …or the old one? Mikhail Girkin Software engineer, Wonga.com
43

Monads - Dublin Scala meetup

Apr 13, 2017

Download

Software

Mikhail Girkin
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Monads - Dublin Scala meetup

Monads An introduction to the brave new world

…or the old one?

Mikhail Girkin

Software engineer, Wonga.com

Page 2: Monads - Dublin Scala meetup

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

Page 3: Monads - Dublin Scala meetup

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

Page 4: Monads - Dublin Scala meetup

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], … )

Page 5: Monads - Dublin Scala meetup

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)

Page 6: Monads - Dublin Scala meetup

… 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(…)

Page 7: Monads - Dublin Scala meetup

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)

Page 8: Monads - Dublin Scala meetup

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?

Page 9: Monads - Dublin Scala meetup

An unexpected move towards for

def outboundFlights(cities: Seq[City]): Seq[String] =

for {

city <- cities

airport <- city.airports

flight <- airport.outboundFlights

} yield flight.code

Page 10: Monads - Dublin Scala meetup

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

Page 11: Monads - Dublin Scala meetup

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

))

)

Page 12: Monads - Dublin Scala meetup

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]

Page 13: Monads - Dublin Scala meetup

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

Page 14: Monads - Dublin Scala meetup

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)

Page 15: Monads - Dublin Scala meetup

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!

Page 16: Monads - Dublin Scala meetup

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

}

Page 17: Monads - Dublin Scala meetup

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! */

}

Page 18: Monads - Dublin Scala meetup

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

Page 19: Monads - Dublin Scala meetup

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)

Page 20: Monads - Dublin Scala meetup

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…

Page 21: Monads - Dublin Scala meetup

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

Page 22: Monads - Dublin Scala meetup

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!

Page 23: Monads - Dublin Scala meetup

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!

Page 24: Monads - Dublin Scala meetup

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

Page 25: Monads - Dublin Scala meetup

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

Page 26: Monads - Dublin Scala meetup

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!

Page 27: Monads - Dublin Scala meetup

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

Page 28: Monads - Dublin Scala meetup

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

Page 29: Monads - Dublin Scala meetup

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

Page 30: Monads - Dublin Scala meetup

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())

}

}

}

Page 31: Monads - Dublin Scala meetup

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)

}

}

Page 32: Monads - Dublin Scala meetup

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

}

Page 33: Monads - Dublin Scala meetup

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

Page 34: Monads - Dublin Scala meetup

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

Page 35: Monads - Dublin Scala meetup

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

}

}

Page 36: Monads - Dublin Scala meetup

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

Page 37: Monads - Dublin Scala meetup

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

Page 38: Monads - Dublin Scala meetup

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

Page 39: Monads - Dublin Scala meetup

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 => ...

...

}

Page 40: Monads - Dublin Scala meetup

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

Page 41: Monads - Dublin Scala meetup

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

Page 42: Monads - Dublin Scala meetup

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)

Page 43: Monads - Dublin Scala meetup

Q&A Thank you!