Подтвердить что ты не робот

Когда и почему следует использовать аппликативные функторы в Scala

Я знаю, что Monad может быть выражен в Scala следующим образом:

trait Monad[F[_]] {
  def flatMap[A, B](f: A => F[B]): F[A] => F[B]
}

Я понимаю, почему это полезно. Например, с учетом двух функций:

getUserById(userId: Int): Option[User] = ...
getPhone(user: User): Option[Phone] = ...

Я могу легко написать функцию getPhoneByUserId(userId: Int), так как Option является монадой:

def getPhoneByUserId(userId: Int): Option[Phone] = 
  getUserById(userId).flatMap(user => getPhone(user))

...

Теперь я вижу Applicative Functor в Scala:

trait Applicative[F[_]] {
  def apply[A, B](f: F[A => B]): F[A] => F[B]
}

Интересно, когда я должен использовать его вместо монады. Я думаю, что оба варианта и списка Applicatives. Не могли бы вы привести простые примеры использования apply с опцией и списком и объяснить, почему я должен использовать его вместо flatMap?

4b9b3361

Ответ 1

Процитировать:

Так зачем вообще заниматься аппликативными функторами, когда у нас есть монады? Прежде всего, просто невозможно предоставить экземпляры monad для некоторых абстракций, с которыми мы хотим работать. Validation - прекрасный пример.

Второй (и связанный с этим), это просто твердая практика развития, чтобы использовать наименее мощную абстракцию, которая выполнит свою работу. В принципе это может позволить оптимизацию, которая в противном случае была бы невозможна, но что более важно, это делает код, который мы пишем, многократно используется повторно.

Чтобы немного расширить первый абзац: иногда у вас нет выбора между монадическим и аппликативным кодом. См. Остальную часть этого ответа для обсуждения того, почему вы можете использовать Scalaz Validation (который не имеет и не может иметь экземпляр монады) для проверки модели.

О точке оптимизации: это, вероятно, будет некоторое время, прежде чем это, как правило, будет актуально в Scala или Scalaz, но см., Например, документацию для Haskell Data.Binary:

Аппликативный стиль иногда может приводить к более быстрому коду, поскольку binary будет пытаться оптимизировать код, объединяя чтения вместе.

Написание аппликативного кода позволяет избежать ненужных заявлений о зависимостях между утверждениями-вычислениями, которые вам поделили бы аналогичный монадический код. Достаточно умная библиотека или компилятор в принципе могли воспользоваться этим фактом.

Чтобы сделать эту идею немного более конкретной, рассмотрим следующий монадический код:

case class Foo(s: Symbol, n: Int)

val maybeFoo = for {
  s <- maybeComputeS(whatever)
  n <- maybeComputeN(whatever)
} yield Foo(s, n)

В for -comprehension desugars к чему - то более или менее, как следующее:

val maybeFoo = maybeComputeS(whatever).flatMap(
  s => maybeComputeN(whatever).map(n => Foo(s, n))
)

Мы знаем, что, возможно, maybeComputeN(whatever) не зависит от s (предполагая, что это хорошие методы, которые не меняют какое-то изменяемое состояние за кулисами), но компилятор не имеет - с его точки зрения он должен знать s до он может начать вычислять n.

Применимая версия (с использованием Scalaz) выглядит следующим образом:

val maybeFoo = (maybeComputeS(whatever) |@| maybeComputeN(whatever))(Foo(_, _))

Здесь мы явно заявляем, что нет никакой зависимости между двумя вычислениями.

(И да, синтаксис |@| довольно ужасен - см. Это сообщение в блоге для обсуждения и альтернатив.)

Но последний момент действительно самый важный. Выбор наименее мощного инструмента, который поможет решить вашу проблему, - это чрезвычайно мощный принцип. Иногда вам действительно нужна монадическая композиция, например, в вашем методе getPhoneByUserId, но часто вы этого не делаете.

Жаль, что и Haskell, и Scala в настоящее время работают с монадами гораздо удобнее (синтаксически и т.д.), Чем работа с аппликативными функторами, но это в основном вопрос исторической катастрофы, а такие события, как скобки идиомы, являются шагом в правильном направление.

Ответ 2

Функтор предназначен для подъема вычислений в категорию.

trait Functor[C[_]] {
  def map[A, B](f : A => B): C[A] => C[B]
}

И он отлично работает для функции одной переменной.

val f = (x : Int) => x + 1

Но для функции от 2 и более после подъема к категории мы имеем следующую сигнатуру:

val g = (x: Int) => (y: Int) => x + y
Option(5) map g // Option[Int => Int]

И это сигнатура аппликативного функтора. И применить следующее значение к функции g - необходим косвенный функтор.

trait Applicative[F[_]] {
  def apply[A, B](f: F[A => B]): F[A] => F[B]
} 

И наконец:

(Applicative[Option] apply (Functor[Option] map g)(Option(5)))(Option(10))

Аппликативный функтор является функтором для применения специального значения (значение в категории) к поднятой функции.