Applicative
Формальное определение
Applicative
расширяет Apply
(и InvariantApplicative
)
и позволяет работать с несколькими «ящиками».
Applicative
, дополнительно к операциям Apply
, реализует операцию unit
(другие названия: point
, pure
),
оборачивающую значение произвольного типа A
в Applicative
.
Для Applicative
должны соблюдаться следующие законы (помимо законов родительских классов типов):
- Identity:
apply(unit(identity))(fa) == fa
(unit
функции идентичности - это идентичность дляapply
) unit(x).map(f) == unit(f(x))
fa.map(f) == apply(unit(f))(fa)
- Homomorphism:
apply(unit(f))(unit(x)) == unit(f(x))
(Другими словами, применение идиоматической функции кunit
такое же, как и кunit
обычного применения функции. Точнее,unit
— это гомоморфизм изA
вF[A]
в отношении применения функции.) - Interchange:
apply(f)(unit(a)) == apply(unit((f: A => B) => f(a)))(f)
(Этот закон, по сути, говорит о том, чтоunit
не разрешено воздействовать на любую реализацию аппликативного функтора. Если один аргумент, который нужно применить, являетсяunit
, то другой может появиться в любой позиции. Другими словами, не должно иметь значения, когда мы выполняемunit
.) - Left Identity:
unit(()).map2(fa)((_, a) => a) == fa
- Right Identity:
fa.map2(unit(()))((a, _) => a) == fa
- Associativity:
fa.product(fb).product(fc) == fa.product(fb.product(fc)).map(assoc)
, гдеdef assoc[A, B, C](p: (A, (B, C))): ((A, B), C) = p match { case (a, (b, c)) => ((a, b), c) }
- Naturality:
fa.map2(fb)((a, b) => (f(a), g(b))) == fa.map(f).product(fb.map(g))
- Composition:
apply(u, apply(v, w)) == apply(apply(apply(unit(f => g => f compose g), u), v), w)
(Применитьv
кw
, а затем применитьu
к этому — то же самое, что применить композицию кu
, затемv
, а затем применить составную функцию кw
.)
Определение в виде кода на Scala
trait Applicative[F[_]] extends Apply[F], InvariantApplicative[F]:
def unit[A](a: => A): F[A]
override def xunit0[A](a: => A): F[A] = unit(a)
def apply[A, B](fab: F[A => B])(fa: F[A]): F[B]
override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
fa.map2(fb)((a, b) => (a, b))
extension [A](fa: F[A])
def map[B](f: A => B): F[B] =
apply(unit(f))(fa)
def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] =
apply(apply(unit(f.curried))(fa))(fb)
Виды Applicative
Applicative с unit
и apply
map2
и map
могут быть выражены так:
trait Applicative[F[_]] extends Functor[F]:
def unit[A](a: => A): F[A]
def apply[A, B](fab: F[A => B])(fa: F[A]): F[B]
extension [A](fa: F[A])
def map2[B,C](fb: F[B])(f: (A, B) => C): F[C] =
apply(apply(unit(f.curried))(fa))(fb)
def map[B](f: A => B): F[B] =
apply(unit(f))(fa)
Applicative с unit
и map2
apply
и map
могут быть выражены так:
trait Applicative[F[_]] extends Functor[F]:
def unit[A](a: => A): F[A]
extension [A](fa: F[A])
def map2[B,C](fb: F[B])(f: (A, B) => C): F[C]
def map[B](f: A => B): F[B] =
fa.map2(unit(()))((a, _) => f(a))
def apply[A, B](fab: F[A => B])(fa: F[A]): F[B] =
fab.map2(fa)((f, a) => f(a))
Комбинаторы Applicative
Applicative
определяет некоторые комбинаторы:
def sequence[A](fas: List[F[A]]): F[List[A]] =
traverse(fas)(identity)
def traverse[A,B](as: List[A])(f: A => F[B]): F[List[B]] =
as.foldRight(unit(List[B]()))((a, acc) => f(a).map2(acc)(_ :: _))
def replicateM[A](n: Int, fa: F[A]): F[List[A]] =
sequence(List.fill(n)(fa))
def product[A, B](fa: F[A], fb: F[B]): F[(A,B)] =
fa.map2(fb)((a, b) => (a, b))
Примеры
Tuple applicative
Как и монады, аппликативные функторы замкнуты относительно произведений; поэтому два независимых аппликативных эффекта обычно могут быть слиты в один, их продукт.
given tupleApplicative[F[_]: Applicative, G[_]: Applicative]: Applicative[[X] =>> (F[X], G[X])] with
type FG[A] = (F[A], G[A])
override def unit[A](a: => A): FG[A] = (summon[Applicative[F]].unit(a), summon[Applicative[G]].unit(a))
override def apply[A, B](fab: FG[A => B])(fa: FG[A]): FG[B] =
(summon[Applicative[F]].apply(fab._1)(fa._1), summon[Applicative[G]].apply(fab._2)(fa._2))
Composite Applicative
В отличие от монад, аппликативные функторы также закрыты по композиции;
поэтому два последовательно зависимых аппликативных эффекта обычно могут быть объединены в один.
Это называется композицией над Applicative
:
given compositeApplicative[F[_]: Applicative, G[_]: Applicative]: Applicative[[X] =>> F[G[X]]] with
override def unit[A](a: => A): F[G[A]] = summon[Applicative[F]].unit(summon[Applicative[G]].unit(a))
override def apply[A, B](fab: F[G[A => B]])(fa: F[G[A]]): F[G[B]] =
val applicativeF = summon[Applicative[F]]
val applicativeG = summon[Applicative[G]]
val tmp: F[G[A] => G[B]] = applicativeF.map(fab)(ga2b => applicativeG.apply(ga2b))
applicativeF.apply(tmp)(fa)
Произведение и композиция позволяют комбинировать идиоматические (аппликативные) вычисления двумя разными способами; Обычно они называются параллельной и последовательной композицией соответственно. Тот факт, что можно составлять аппликативы, и они остаются аппликативными, очень полезен.
"Обертка"
case class Id[A](value: A)
given idApplicative: Applicative[Id] with
override def unit[A](a: => A): Id[A] = Id(a)
override def apply[A, B](fab: Id[A => B])(fa: Id[A]): Id[B] = Id(fab(fa))
Option
given optionApplicative: Applicative[Option] with
override def unit[A](a: => A): Option[A] = Some(a)
override def apply[A, B](fab: Option[A => B])(fa: Option[A]): Option[B] =
(fab, fa) match
case (Some(aToB), Some(a)) => Some(aToB(a))
case _ => None
Последовательность
given listApplicative: Applicative[List] with
override def unit[A](a: => A): List[A] = List(a)
override def apply[A, B](fab: List[A => B])(fa: List[A]): List[B] =
fab.flatMap { aToB => fa.map(aToB) }
Either
given eitherApplicative[E]: Applicative[[x] =>> Either[E, x]] with
override def unit[A](a: => A): Either[E, A] = Right(a)
override def apply[A, B](fab: Either[E, A => B])(fa: Either[E, A]): Either[E, B] =
(fab, fa) match
case (Right(fx), Right(a)) => Right(fx(a))
case (Left(l), _) => Left(l)
case (_, Left(l)) => Left(l)
Writer - функциональный журнал
case class Writer[W, A](run: () => (W, A))
trait Semigroup[A]:
def combine(x: A, y: A): A
trait Monoid[A] extends Semigroup[A]:
def empty: A
given writerApplicative[W](using monoid: Monoid[W]): Applicative[[x] =>> Writer[W, x]] with
override def unit[A](a: => A): Writer[W, A] =
Writer[W, A](() => (monoid.empty, a))
override def apply[A, B](fab: Writer[W, A => B])(fa: Writer[W, A]): Writer[W, B] =
Writer { () =>
val (w0, aToB) = fab.run()
val (w1, a) = fa.run()
(monoid.combine(w0, w1), aToB(a))
}
State - функциональное состояние
case class State[S, +A](run: S => (S, A))
given stateApplicative[S]: Applicative[[x] =>> State[S, x]] with
override def unit[A](a: => A): State[S, A] =
State[S, A](s => (s, a))
override def apply[A, B](fab: State[S, A => B])(fa: State[S, A]): State[S, B] =
State { s =>
val (s0, aToB) = fab.run(s)
val (s1, a) = fa.run(s0)
(s1, aToB(a))
}
Nested
final case class Nested[F[_], G[_], A](value: F[G[A]])
given nestedApplicative[F[_], G[_]](using
applF: Applicative[F],
applG: Applicative[G],
functorF: Functor[F]
): Applicative[[X] =>> Nested[F, G, X]] with
override def unit[A](a: => A): Nested[F, G, A] = Nested(applF.unit(applG.unit(a)))
override def apply[A, B](fab: Nested[F, G, A => B])(fa: Nested[F, G, A]): Nested[F, G, B] =
val curriedFuncs: G[A => B] => G[A] => G[B] = gaTob => ga => applG.apply(gaTob)(ga)
val fgaToB: F[G[A => B]] = fab.value
val fGaToGb: F[G[A] => G[B]] = functorF.map(fgaToB)(curriedFuncs)
val fga: F[G[A]] = fa.value
val fgb: F[G[B]] = applF.apply(fGaToGb)(fga)
Nested(fgb)
IO
final case class IO[R](run: () => R)
given ioApplicative: Applicative[IO] with
override def unit[A](a: => A): IO[A] = IO(() => a)
override def apply[A, B](fab: IO[A => B])(fa: IO[A]): IO[B] = IO(() => fab.run()(fa.run()))
Реализация
Реализация в Cats
import cats.*, cats.data.*, cats.syntax.all.*
Applicative[List].pure(1) // List(1)
Applicative[Option].pure(1) // Some(1)
Реализация в ScalaZ
import scalaz.*
import Scalaz.*
// ... Все операции родителей
1.point[List] // List(1)
1.η[List] // List(1)
Ссылки:
- Algebird
- Applicative Programming with Effects
- Cats
- Functional Programming in Scala, Second Edition
- Functional Programming in Scala, Second Edition, Wiki
- Herding Cats
- Learn Functional Programming course/tutorial on Scala
- Learning Scalaz
- Scala with Cats
- Scalaz API
- SKB – Scala Applicative
- Tour of Scala