
Нека да генерализираме познатите ни от тях операции в type class-ове
Предния път постигнахме подобна генерализация за обикновени типове, нека сега се опитаме да го направим и за ефекти.
Ще започнем от една различна гледна точка
Нека имаме функции f: A => B и g: B => C
Тогава h(x) = g(f(x)) е функция от тип A => C
h = g ∘ f
асоциативност – нека f: A => B, g: B => C и h: C => D. Тогава:
(h ∘ g) ∘ f = h ∘ (g ∘ f)неутрален елемент – нека identity = x => x. Тогава ∀ f
identity ∘ f = f ∘ identity = fh ∘ g ∘ f
Функция, връщаща стойност, затворена в ефект
A => Option[B]
A => Future[B]
A => Validated[E, B]
Наричат се още Kleisli arrows
Нека
f: A => Option[B],
g: B => Option[C],
h: C => Option[D]
h ∘ g ∘ f?
За всеки ефект имплементацията е различна
flatMapdef compose[A, B, C, D](f: A => Option[B],
g: B => Option[C],
h: C => Option[D]): A => Option[D] = a =>
val fOption = f(a)
if fOption != None then
val gOption = g(fOption.get)
if (gOption != None) then
h(gOption.get)
else
None
else
None
Често срещано при работа с някои езикови елементи (null, callback hell код, …)
trait Monad[F[_]]:
def compose[A, B, C](f: A => F[B], g: B => F[C]): A => F[C]
def unit[A](a: A): F[A]
Тук F е конструктор на тип, а не тип
Пример: List е конструктор на тип, List[Int] е тип
F е higher-kinded type (тип от по-висок ред)
higher-kinded polymorphism
асоциативност:
compose(compose(f, g), h) == compose(f, compose(g, h))неутрален елемент, за който имаме left identity & right identity
compose(unit, f) == compose(f, unit) == fМного приличат на аксиомите за моноид, но с функции
A monad is just a monoid in the category of endofunctors, what’s the problem? - A Brief, Incomplete, and Mostly Wrong History of Programming Languages
flatMapflatMapасоциативност:
Нека m: F[A] и f: A => F[B], g: B => F[C]. Тогава
m.flatMap(f).flatMap(g) == m.flatMap(a => f(a).flatMap(g))ляв идентитет:
∀a: A и f: A => F[B] е изпълнено: unit(a).flatMap(f) == f(a)
десен идентитет:
∀m: F[A] е изпълнено: m.flatMap(unit) == m
Нека имплементираме още няколко допълнителни операции към монадата - exercises/effects/Monad.scala
Наша имплементация на Option - ще използваме името Maybe за да избегнем колизия с името от стандартната библиотека
unit, map и flatten (aka join) са трети възможен набор от основни операции
Монадата ни позволява да опишем последователни изчисления върху ефекти
Или още казано - да композираме ефектни изчисления едно след друго
unit и flatMapunit и composeunit, map, и flatteneffects/id/Id.scala
effects/state/State.scala
Някои монади си имат и грешни състояния, които биха прекъснали цялата композиция
Примери:
Option - NoneEither - Left(error)Try - Failure(throwable)Композицията става по-трудна ако работим с библиотеки, които ползват различни монади за връщане на грешките
def readFile(): Try[String] = ???
def toJson(str: String): Either[Throwable, Json] = ???
for
fileContent <- readFile()
parsedFile <- toJson(fileContent) //does not compile
yield parsedFile Това също може да се генерализира като използваме MonadError[F[_], E], но няма да го разглеждаме подробно.
unit & map2 за основни операцииtrait Applicative[F[_]] extends Functor[F]:
// primitive
def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C]
def unit[A](a: => A): F[A]
// derived
def map[A, B](fa: F[A])(f: A => B): F[B] = map2(fa, unit(()))((a, _) => f(a))
def zip[A,B](fa: F[A], fb: F[B]): F[(A,B)] = map2(fa, fb)((_,_))Алтернативни базови операции са map, zip, unit
Applicative
Monad
val F: Monad[Option] = ???
val idsByName: Map[String, Int] = ???
val depts: Map[Int, String] = ???
val salaries: Map[Int, Double] = ???
// the results of one lookup affect what lookup we do next
val o: Option[String] =
idsByName.get("Bob").flatMap { id =>
F.map2(depts.get(id), salaries.get(id))(
(dept, salary) => s"Bob in $dept makes $salary per year"
)
}join или flatMapБихме могли да дефинираме и повече от map2
Когато не знаем колко е N
Примери: effects/ApplicativeSequenceDemo & effects/ApplicativeTraverseDemo
sequence & traverse виждаме конкретен тип, който може да бъде генерализиранList - нека се абстрахираме от негоtrait Traversable[F[_]] extends Functor[F]:
def traverse[G[_] : Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
def sequence[G[_] : Applicative, A](fga: F[G[A]]): G[F[A]] =
traverse(fga)(ga => ga)traverse също може да се изрази чрез sequence
map може да се изрази чрез traverse
Функторите могат да бъдат композирани:
Апликативите също
import cats.data.Nested
import cats.implicits.*
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val x: Future[Option[Int]] = Future.successful(Some(5))
val y: Future[Option[Char]] = Future.successful(Some('a'))
val composed = Applicative[Future].compose[Option].map2(x, y)(_ + _)
// composed: Future[Option[Int]] = Future(Success(Some(102)))В общия случай монадите не могат да се композират. Но много могат
Това води до нуждата от специфични монадни трансформатори
Например OptionT за монади от Option
(тоест M[Option[_]], където M е монада)