Монады

Яценко Иван
                        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]
                        
def mapF[A, B](f: A => B): F[A] => F[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 low
                                    def identity[A](x: A):A = x
    
                                    List(1, 2, 3).map(identity) == identity(List(1, 2, 3)) == List(1, 2, 3)
                                
  • Associativity low
                                    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) {
                              Nil
                            } else {
                              val result = ListBuffer.empty[B]
                              for(i <- 0 to list.size - 1) {
                                result.append(f(list(i)))
                              }
                              result.toList
                            }
                          }
                        }
                    

List-Функтор

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

                        // 2. Associativity low
                        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")
                    

Функтор

Использование для потенциально ошибочных вычислений
                        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) }

Функтор

Использование для потенциально ошибочных вычислений
                        def divide2(a: Int, b1: Int, b2: Int): Try[Try[Int]] = 
                          divide(a, b1).map(divide(_, b2))
                    
                        def sequentialDivision(a: Int, b1: Int, bs: Int *) = bs match {
                          case head :: tail => divide(a, b1).map(sequentialDivision(_, head, tail))
                          case _ => divide(a, b1)
                        }
                    
                        trait 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(m)(unit) = m)
                                    optionX.flatMap(Option(_)) == optionX
                                
  • Left unit law (bind(unit(x))(foo) == 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 low
                        val optionX = Option(12)   // Some(12)
                        optionX.flatMap(Option(_)) // Some(12)
                        None.flatMap(Option(_))    // None

                        // 2. Left unit low
                        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 low
                        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

for-выражение не ограничивается только работой со списками. Каждый тип данных, поддерживающий операции withFilter, map, и flatMap (с соответствующими типами).

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!"
                    

Зачем:

  • Работа с чистыми значениями

    Монада и функторы - чистые значения, внутри которых может лежать что угодно (эффекты, мутабельные данные)

  • Управление порядком вычислений

    При ленивых и ассинхронных вичислениях можно указывать строгий порядок без callback'ов

  • Более "чистый" код

Cats

Scalaz