Монады

Роман Деньгин
                        def mapList[A, B](f: A => B): List[A] => List[B] = { (listA: List[A]) =>

                          def mapImpl(input: List[A], buffer: List[B]): List[B] = input match {
                            case head :: tail => mapImpl(tail, buffer :+ f(head))
                            case _            => buffer
                          }

                          mapImpl(listA, Nil)
                        }
                    
                        def mapOption[A, B](f: A => B): Option[A] => Option[B] = {
                          case Some(a) => Some(f(a))
                          case _       => None
                        }
                    
                        def mapFuture[A, B](f: A => B): Future[A] => Future[B]
                        def mapTry[A, B](f: A => B): Try[A] => Try[B]
                        def mapReads[A, B](f: A => B): Reads[A] => Reads[B]
                    

Функтор

Функтор - класс типов F[_], на которых можно определить операцию
(A ⇒ B) ⇒ F[A] ⇒ F[B]
                        trait Functor[F[_]] {
                          def map[A, B](f: A => B): F[A] => F[B]
                        }

                        map(_.doubleValue)(List(1, 2, 3)) // List(2.0, 4.0, 6.0)
                        map(_.toString)(map(_.doubleValue)(List(1, 2, 3))) // List("2.0", "4.0", "6.0")
                    

Функтор

                        trait FunctorInterface[A] {
                          def map[B](f: A => B): FunctorInterface[B]
                        }
  
                        Functor[List].map(_.doubleValue)(List(1, 2, 3)) ~ List(1, 2, 3).map(_.doubleValue)
                    

Функтор

  • Identity law
                                    def identity[A](x: A):A = x
    
                                    List(1, 2, 3).map(identity) == identity(List(1, 2, 3)) == List(1, 2, 3)
                                
  • Associativity law
                                    val intToDouble: Int => Double
                                    val doubleToString: Double => String
    
                                    List(1, 2, 3).map(intToDouble).map(doubleToString) 
                                      == List(1, 2, 3).map(intToDouble andThen doubleToString) 
                                      == List("1.0", "2.0", "3.0")
                                

List-Функтор

                        val listFunctor: Functor[List] = new Functor[List] {
                          def map[A, B](f: A => B): List[A] => List[B] = { (list: List[A]) =>
                            if(list.isEmpty) {
                              List.empty[B]
                            } else {
                              val buffer = ListBuffer.empty[B]
                              for(i <- 0 until list.size) {
                                buffer.append(f(list(i)))
                              }
                              buffer.toList
                            }
                          }
                        }
                    

List-Функтор

                        // 1. Identity law
                        List(1, 2, 3).map(identity) // List(1, 2, 3)

                        // 2. Associativity law
                        List(1, 2, 3)
                          .map(intToDouble)        // List(1.0, 2.0, 3.0)
                          .map(doubleToString)     // List("1.0", "2.0", "3.0")

                        List(1, 2, 3)
                          .map(itToDouble andThen doubleToString)  // List("1.0", "2.0", "3.0")
                    

Try-Функтор

                        sealed trait Try[+T]
                        final case class Success[+T](value: T) extends Try[T]
                        final case class Failure[+T](exception: Exception) extends Try[T]
                        
def divide(a: Int, b: Int): Try[Int] = try Success(a / b) catch { case NonFatal(e) => Failure(e) // Не выбрасываем исключение => нет побочного эффекта }

Try-Функтор

                        def printResult(result: Try[Int]) = result match {
                          case Success(n) => println(n)
                          case Failure(e) => printnl(s"An error ${e.message}")
                        }

                        def divide2(a: Int, b1: Int, b2: Int): Try[Try[Int]] = 
                          divide(a, b1).map(divide(_, b2))

                        def printResult(result: Try[Try[Int]]) = result match {
                          case Success(Success(n)) => println(n)
                          case Failure(e) => printnl(s"An error ${e.message}")
                        }


                        def sequentialDivision(a: Int, b1: Int, bs: Int *) = bs match {
                          case head :: tail => divide(a, b1).map(sequentialDivision(_, head, tail))
                          case _ => divide(a, b1)
                        }
                    
                        def Bind[F[_]] {
                          def >>=[A, B](f: A => F[B]): F[A] => F[B]
                          def flatMap[A, B](f: A => F[B]): F[A] => F[B]
                        }
                        
def sequentialDivision(a: Int, b1: Int, bs: Int *): Try[Int] = bs match { case head :: tail => divide(a, b1).flatMap(sequentialDivision(_, head, tail)) case _ => divide(a, b1) }

Монада

Монада - класс функторов M[_], на которых можно определить операцию
(A ⇒ M[B]) ⇒ M[A] ⇒ M[B]
                        trait Monad[M[_]] extends Functor[M] {
                          def unit[A](x: A): M[A]

                          def flatMap[A, B](f: A => M[B]): M[A] => M[B]
                          override def map[A, B](f: A => B): M[A] => M[B] = flatMap(f andThen unit)
                        }
                    

Монада

  • Right unit law (bind(unit)(m) = m)
                                    optionX.flatMap(Option(_)) == optionX
                                
  • Left unit law (bind(foo)(unit(x)) == foo(x))
                                    Option(x).flatMap(foo) == foo(x)
                                
  • Associativity law (bind(bind(m)(foo))(bar) = bind(m)(x => bind(foo(x))(bar)))
    optionX.flatMap(foo).flatMap(bar) == optionX.flatMap(x => foo(x).flatMap(bar))
                            

Option-Монада

                        val optionMonad: Monad[Option] = new Monad[Option] {
                          def unit[A](x: A) = if(x == null) None else Some(x)

                          def flatMap[A, B](f: A => Option[B]): Option[A] => Option[B] = {
                            case Some(a) => f(a)
                            case _       => None
                          }
                        }
                    

Option-Монада

                        // 1. Right unit law
                        val optionX = Option(12)   // Some(12)
                        optionX.flatMap(Option(_)) // Some(12)
                        None.flatMap(Option(_))    // None

                        // 2. Left unit law
                        val intHalf: Int => Option[Int] = x => if(x % 2 == 0) Some(x / 2) else None
                        Option(5).flatMap(intHalf) // None
                        intHalf(5)                 // None
                        Option(4).flatMap(intHalf) // Some(4)
                        intHalf(4)                 // Some(4)

                        // 3. Associativity law
                        val intThird: Int => Option[Int] = x => if(x % 3 == 0) Some(x / 3) else None
                        optionX.flatMap(intHalf).flatMap(intThird)         // Some(2)
                        optionX.flatMap(x => intHalf(x).flatMap(intThird)) // Some(2)
                    

Монады в scala

  • List[T], Option[T]...
  • Future[T] - ассинхронное выполнение кода
  • Either[A, B] - сумма монад Left[A] и Right[B]

for-yield

                        val foo = (for(i <- 0 to x) yield i * i) // Range(0*0, 1*1, ..., i*i)
                        val bar = for(i <- 0 to x; j <- 0 to y) yield i * j // Range(0*0, 0*1, ..., 0*j, 1*0, 1*1,..., i*j)

                        foo ~ (0 to x).map(i => i * i)

                        bar ~ (0 to x).flatMap(i => (0 to y).map(j => i * j))
                    

for-yield

fooM
  .flatMap { foo1 =>
    f1(foo1)
      .flatMap { foo2 =>
        f2(foo2)
          .flatMap(foo3 => f3(foo3))
      }
  }
~
for {
  foo1 <- fooM
  foo2 <- f1(foo1)
  foo3 <- f2(foo2)
  x    <- f3(foo3)
} yield x

                                

for-yield

                        case class BucketBook(userId: UUID, bookId: UUID)
                        case class Book(id: UUID, authorId: UUID)

                        def getUserBucket(userId: UUID): Future[Seq[BucketBook]]
                        def getBooks(bookIds: Seq[UUID]): Future[Seq[Book]]
                        def getAuthors(authorIds: Seq[UUID]): Future[Seq[Author]]
                    

for-yield

                        def bucketAuthors(userId: UUID): Future[Seq[Author]] =
                          getUserBucket(userID)
                          .flatMap { bucketBooks =>

                            val bookIds = bucketBooks.map(_.bookId)

                            getBooks(bookIds)
                              .flatMap { books =>

                                val authorIds = books.map(_.authorId)
        
                                getAuthors(authorIds)
                              }
                          }
                    

for-yield

                        def bucketAuthors(userId: UUID): Future[Seq[Author]] =
                          for {
                            bucketBooks <- getUserBucket(userID)
                            bookIds = bucketBooks.map(_.bookId)

                            books <- getBooks(bookIds)
                            authorIds = books.map(_.authorId)

                            authors <- getAuthors(authorIds)
                          } yield authors
                    

IO

IO - монада, кторая используется для функций с побочным эффектом.
                        scala> val ioa: IO[Unit] = IO { println("Hello, world!") }
                        ioa: IO[Unit]
                        scala> val program: IO[String] =
                        | for {
                        |    _ <- ioa
                        |    _ <- ioa
                        | } yield "Done!"
                        program: IO[String]
                        scala> program.unsafeRunSync()
                        Hello, world!
                        Hello, world!
                        res0: String = "Done!"
                    

Сторонние библиотеки

Cats is a library which provides abstractions for functional programming in the Scala programming language. The name is a playful shortening of the word category.
[Cats - библиотека, предоставляющая абстракции функционального программирования в Scala. Cats - игривое сокращение от слова "КАТегория"]
It provides purely functional data structures to complement those from the Scala standard library
[Предоставляет "чисто"-функциональные структуры данных для стандартной библиотеки Scala]

Сторонние библиотеки

  • Безопасность
    учтены многие недочеты стандартной библиотеки;
  • Расширенный функционал
    IO, mapN (map для TupleN[F[_], F[_], ...]), безопасные преобразования типов и т.д.;
  • Совместимость
    http4s, fs2, zio;
  • API в "более функциональном стиле"
    реализации с синтаксисом вида `>>=`, `>*` и т.д.

Спасибо за внимание

Вопросы?..