📜 ⬆️ ⬇️

Monads in Scala

On Habré, there are many articles on monads with examples for Haskell ( http://habrahabr.ru/post/183150 , http://habrahabr.ru/post/127556 ), but not many articles that describe what monads are with examples on Scala. Since the majority of Scala developers came from the world of object-oriented programming, then, for them, at first, it is difficult to understand what monads are and what they are for, this article is for such developers. In this article I want to show what it is and bring examples of using the Option monad, in the next articles the Try and Future monads will be described.

So, a monad is a parametric data type that necessarily implements two operations: the creation of a monad (in the literature the function unit) - and the function flatMap () (in the literature it is sometimes called bind) and obeys some rules. They are used to implement the strategy of linking calculations. Let's give an example of the simplest monad:

trait Monad[T] { def flatMap[U](f: T => Monad[U]): Monad[U] } def unit[T](x: T): Monad[T] 


The flatMap function accepts a function for input, which accepts data that is placed in a monad (the monad is a container) and returns a new monad. It is worth noting that a function can return a monad of another type (U instead of T), as will be shown later - this is a very useful thing.
')
As for the function unit , it is responsible for creating the monad and for each monad it is different. For example, the function unit.

Option Some(x)
List List(x)
Try Success(x)

For each monad, you can define a map function and express it through the combination flatMap and unit . For example:

 def mapExample() { val monad: Option[Int] = Some(5) assert(monad.map(squareFunction) == monad.flatMap(x => Some(squareFunction(x)))) } 

Also, each monad must obey 3 laws, and they must ensure that the monadic composition works in a predictable way. We will check these laws on the monad Option.

To begin with, we will define two simple functions that we will use in for checking, these are squares and increments, they return Option, this is done to be able to transfer them to the flatMap and for further composition.

  def squareFunction(x: Int): Option[Int] = Some(x * x) def incrementFunction(x: Int): Option[Int] = Some(x + 1) 

The first law is called Left unit law and it looks like this:

unit(x) flatMap f == f(x)

And he says that if you use the flatMap function for a type with a positive value (for Option is Some) and pass some function there, the result will be the same as simply applying this function to a variable. This is better demonstrated by the code below:

 def leftUnitLaw() { val x = 5 val monad: Option[Int] = Some(x) val result = monad.flatMap(squareFunction) == squareFunction(x) println(result) } 


As expected, the result will be true .

The second law is called Right unit law and looks like this:

monad flatMap unit == monad

And he says that if we pass a function to the flatMap that creates a monad from the data (those that are in the monad), then at the output we get the same monad.

 def rightUnitLaw() { val x = 5 val monad: Option[Int] = Some(x) val result = monad.flatMap(x => Some(x)) == monad println(result) } 


The flatMap function opens monad and gets x and passes it to the function x => Some(x) which constructs the new monad. If the monad variable monad assigned the value None , the result will still be true , because flatMap will simply return None and will not call the function passed to it.

The third law is called Associativity law :

(monad flatMap f) flatMap g == monad flatMap(x => f(x) flatMap g)

If you write it on Scala:

  def associativityLaw() { val x = 5 val monad: Option[Int] = Some(x) val left = monad flatMap squareFunction flatMap incrementFunction val right = monad flatMap (x => squareFunction(x) flatMap incrementFunction) assert(left == right) } 


And this observance of this law gives us the right to use for comprehension in its usual form, that is, instead of:

 for (square <- for (x <- monad; sq <- squareFunction(x)) yield sq; result <- incrementFunction(square)) yield result 

We can write:

 for (x <- monad; square <- squareFunction(x); result <- incrementFunction(square)) yield result 


And so, all these laws give us the fact that we can encapsulate the logic of a chain of computations, which is what we need monads for if we believe Wikipedia . This is very clearly seen when applying the monad of Future and actors, but this is the topic of a separate article. To demonstrate the chain of calculations, we will create two simple functions for calculating the port and server host and write them to return a positive result Some . And the creation of an InetSocketAddress depending on the results of the work of these functions.

  def findPort(): Option[Int] = Some(22) def findHost(): Option[String] = Some("my.host.com") val address: Option[InetSocketAddress] = for { host <- findHost() port <- findPort() } yield new InetSocketAddress(host, port) println(address) 


The result of the execution of this code will be something like: Some(my.host.com/82.98.86.171:22) . Note that yield also returns Option to use it for further calculations. In order to get the address itself, use the map function and display the result, if any of the functions in the chains of calculations returns None then the total result will also be None .

 address.map(add => println("Address : " + add)).getOrElse(println("Error")) // Address : my.host.com/82.98.86.171:22 


For practical use of monads, you should first of all remember that flatMap and map will never be executed with negative input data (for Option this is None ). The use of these functions greatly simplifies the fight against errors.

Source: https://habr.com/ru/post/209510/


All Articles