Applicative

Формальное определение

Applicative расширяет ApplyInvariantApplicative) и позволяет работать с несколькими «ящиками». Applicative, дополнительно к операциям Apply, реализует операцию unit (другие названия: point, pure), оборачивающую значение произвольного типа A в Applicative.

Для Applicative должны соблюдаться следующие законы (помимо законов родительских классов типов):

Определение в виде кода на 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)

Ссылки: