Нека да генерализираме познатите ни от тях операции в 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 = f
h ∘ 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?
За всеки ефект имплементацията е различна
flatMap
def 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
flatMap
flatMap
асоциативност:
Нека 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
и flatMap
unit
и compose
unit
, map
, и flatten
effects/id/Id.scala
effects/state/State.scala
Някои монади си имат и грешни състояния, които биха прекъснали цялата композиция
Примери:
Option
- None
Either
- 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
е монада)