📜 ⬆️ ⬇️

Unscientific about monads

Hello.

After four years of programming in Scala, my understanding of monads has finally grown to a level where it can be explained to others without reference to category theory and the classical monad - this is just a monoid in the category of endofunctors that frightens programmers no worse than cockroach dichlorvos.

Code examples will be written in the Kotlin language, since it is quite popular, and at the same time quite functional (in both senses of the word).

Let's start with the notion of a functor , here it is:
')
interface Functor<A> 

What is its meaning? A functor is an abstraction over an arbitrary calculation (computation) that returns a result of type A. We abstract from how to create a new functor, and, most importantly, from how to calculate its value A. In particular, a function can be hidden behind the interface of a functor. with an arbitrary number of arguments, and not necessarily a pure function.

Examples of implementations of the functor:


All these examples, except for the constant, have one important property - they are lazy, i.e. the computation itself does not occur when the functor is created, but when it is computed.

The functor interface does not allow either to get the value of type A from Functor<A> , nor to create a new Functor<A> based on the existing value of type A But even with such restrictions, the functor is useful - if for some type B we can convert A to B (in other words, there is a function (a: A) -> B ), then we can write a function (f: Functor<A>) -> Functor<B> and name its map :

 interface Functor<A> { fun <B> map(f: (A) -> B): Functor<B> } 

Unlike the functor itself, the map method cannot be an arbitrary function:
- map((a) -> a) must return the same functor
- map((a) -> f(a)).map((b) -> g(b)) must be identical to map(a -> g(f(a))

As an example, we implement a functor that returns the value of A, which contains a certain number of random bits. Our interface in Kotlin will not be so easy to use (but if you prefer, you can ), so we will write the extension method:

 //  - ,     ,   map data class MyRandom<A>( val get: (bits: Int) -> A ) { companion object { val intRandom: MyRandom<Int> = MyRandom { Random.nextBits(it) } val hexRandom: MyRandom<String> = intRandom.map { it.toString(16) } } } //  map   fun <A, B> MyRandom<A>.map(f: (A) -> B): MyRandom<B> = MyRandom(get = {bits -> f(get(bits)) }) fun main(args: Array<String>) { println("random=" + MyRandom.intRandom.get(12)) //  random=1247 println("hexRandom=" + MyRandom.hexRandom.get(12)) //  hexRandom=c25 } 

Other examples of functors with a useful map


Now you can go to monads.

A monad is a functor with two additional operations. First of all, the monad, in contrast to the functor, contains the creation operation from a constant, this operation is called lift :

 fun <A> lift(value: A): Monad<A> = TODO() 

The second operation is called flatMap , it is more complicated, so first we give our entire monad interface:

 interface Monad<A> { //   ,  map     - //    flatMap  lift fun <B> map(f: (A) -> B): Monad<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Monad<B>): Monad<B> } fun <A> lift(value: A): Monad<A> = TODO() 

The most important difference between a monad and a functor is that monads can be combined with each other, generating new monads while abstracting from how the monad is implemented — whether it reads from disk, accepts additional parameters to calculate its value, whether it exists at all . The second important point is that monads are not combined in parallel, but in series, leaving the possibility to add logic depending on the result of the first monad.

Example:

 // ,     Int //       -      //           val readInt: Monad<Int> = TODO() // ,      -  fun readBytes(len: Int): Monad<ByteArray> = TODO() // ,     ,    val bytes: Monad<ByteArray> = readInt.flatMap {len -> if (len > 0) readBytes(len) //    -   else lift(ByteArray(0)) //  ,    } 

However, in this example there is no mention of the network. With the same success data can be read from a file or from a database. They can be read synchronously or asynchronously, there may be error handling here - everything depends on the specific implementation of the monad, the code itself will remain unchanged.

First, a simpler example, the Monad Option. In cotlin, it is not really needed, but in Java / Scala it is extremely useful:

 data class Option<A>(val value: A?) { fun <B> map(f: (A) -> B): Option<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Option<B>): Option<B> = when(value) { null -> Option(null) else -> f(value) } } fun <A> lift(value: A?): Option<A> = Option(value) fun mul(a: Option<Int>, b: Option<Int>): Option<Int> = a.flatMap { a -> b.map { b -> a * b } } fun main(args: Array<String>) { println(mul(Option(4), Option(5)).value) // 20 println(mul(Option(null), Option(5)).value) // null println(mul(Option(4), Option(null)).value) // null println(mul(Option(null), Option(null)).value) // null } 

As a monad of pozakovyristey, let's wrap up in the monad work with the database:

 data class DB<A>(val f: (Connection) -> A) { fun <B> map(f: (A) -> B): DB<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> DB<B>): DB<B> = DB { conn -> f(this.f(conn)).f(conn) } } fun <A> lift(value: A): DB<A> = DB { value } fun select(id: Int): DB<String> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun update(value: String): DB<Unit> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun selectThenUpdate(id: Int): DB<Unit> = select(id).flatMap { value -> update(value) } fun executeTransaction(c: Connection): Unit { //  ,     //          val body: DB<Unit> = selectThenUpdate(42) //  ,   select  update body.f(c) c.commit() } 

Is the rabbit hole deep?


There are a huge variety of monads, but their main purpose is to abstract the business logic of the application from some of the details of the calculations:


The list of questions left unlit:


What's next?


arrow-kt.io
typelevel.org/cats/typeclasses.html
wiki.haskell.org/All_About_Monads

My experiment is a full-fledged application in FP style on Scala:
github.com/scf37/fpscala2

PS I wanted a small note, it turned out as always.

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


All Articles