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

Monads - Dublin Scala meetup

Embed Size (px)

Citation preview

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)

Q&A Thank you!