web-dev-qa-db-fra.com

Quand et pourquoi utiliser des foncteurs applicatifs dans Scala

Je sais que Monad peut s'exprimer en Scala comme suit:

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

Je vois pourquoi c'est utile. Par exemple, compte tenu de deux fonctions:

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

Je peux facilement écrire la fonction getPhoneByUserId(userId: Int) puisque Option est une monade:

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

...

Maintenant, je vois Applicative Functor Dans Scala:

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

Je me demande quand je devrais l'utiliser au lieu de monade. Je suppose que Option et List sont Applicatives. Pourriez-vous donner des exemples simples d'utilisation de apply avec Option et List et expliquer pourquoi Je devrais l'utiliser au lieu deflatMap?

65
Michael

Pour me citer :

Alors pourquoi s'embêter avec des foncteurs applicatifs quand nous avons des monades? Tout d'abord, il n'est tout simplement pas possible de fournir des instances de monade pour certaines des abstractions avec lesquelles nous voulons travailler - Validation est l'exemple parfait.

Deuxièmement (et corrélativement), c'est juste une pratique de développement solide pour utiliser l'abstraction la moins puissante qui fera le travail. En principe, cela peut permettre des optimisations qui ne seraient pas possibles autrement, mais plus important encore, cela rend le code que nous écrivons plus réutilisable.

Pour développer un peu le premier paragraphe: parfois, vous n'avez pas le choix entre du code monadique et du code applicatif. Voir le reste de cette réponse pour une explication de la raison pour laquelle vous voudrez peut-être utiliser Validation de Scalaz (qui n'a pas et ne peut pas avoir d'instance monade) pour modéliser la validation.

À propos du point d'optimisation: il faudra probablement un certain temps avant que cela ne soit généralement pertinent dans Scala ou Scalaz, mais voir par exemple la documentation pour Data.Binary De Haskell) :

Le style applicatif peut parfois entraîner un code plus rapide, car binary tentera d'optimiser le code en regroupant les lectures.

L'écriture de code applicatif vous permet d'éviter de faire des affirmations inutiles sur les dépendances entre les calculs, affirmations auxquelles un code monadique similaire vous obligerait. Une bibliothèque ou un compilateur suffisamment intelligent pourrait en principe profiter de ce fait.

Pour rendre cette idée un peu plus concrète, considérons le code monadique suivant:

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

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

Le for - compréhension desugars vers quelque chose de plus ou moins comme ceci:

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

Nous savons que maybeComputeN(whatever) ne dépend pas de s (en supposant que ce sont des méthodes bien comportées qui ne changent pas un état mutable en arrière-plan), mais le compilateur ne le fait pas - son point de vue, il doit connaître s avant de pouvoir commencer à calculer n.

La version applicative (utilisant Scalaz) ressemble à ceci:

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

Ici, nous déclarons explicitement qu'il n'y a pas de dépendance entre les deux calculs.

(Et oui, cette syntaxe |@| Est assez horrible - voir ce billet de blog pour une discussion et des alternatives.)

Mais ce dernier point est vraiment le plus important. Choisir l'outil le moins puissant qui résoudra votre problème est un principe extrêmement puissant. Parfois, vous avez vraiment besoin d'une composition monadique - dans votre méthode getPhoneByUserId, par exemple - mais souvent vous n'en avez pas.

Il est dommage que Haskell et Scala rendent actuellement le travail avec les monades tellement plus pratique (syntaxiquement, etc.) que de travailler avec des foncteurs applicatifs, mais c'est surtout une question d'accident historique et de développements comme les crochets idiomatiques sont un pas dans la bonne direction.

77
Travis Brown

Functor sert à élever des calculs à une catégorie.

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

Et cela fonctionne parfaitement pour une fonction d'une variable.

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

Mais pour une fonction de 2 et plus, après avoir atteint une catégorie, nous avons la signature suivante:

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

Et c'est la signature d'un foncteur applicatif. Et pour appliquer la valeur suivante à une fonction g - un foncteur aplicatif est nécessaire.

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

Et enfin:

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

Le foncteur applicatif est un foncteur permettant d'appliquer une valeur spéciale (valeur dans la catégorie) à une fonction levée.

24
Yuriy