Quand et pourquoi utiliser les foncteurs applicatifs à Scala

Je sais que Monad peut être exprimé 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, étant donné 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 un Applicative Functor à 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 monad. Je suppose que Option et Liste sont des Applicatives . Pourriez-vous donner des exemples simples d’utilisation d’ flatMap avec Option et Liste et expliquer pourquoi je devrais l’utiliser à la place de flatMap ?

    Pour me citer :

    Alors pourquoi se préoccuper des foncteurs applicatifs, quand on a des monades? Tout d’abord, il n’est tout simplement pas possible de fournir des exemples de monade pour certaines des abstractions avec lesquelles nous voulons travailler. La Validation est l’exemple parfait.

    Deuxièmement (et dans le même ordre d’idées), c’est simplement une pratique de développement solide qui consiste à utiliser l’abstraction la moins puissante pour faire 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: vous n’avez parfois pas le choix entre le code monadique et le code applicatif. Voyez le rest de cette réponse pour une discussion sur les raisons pour lesquelles vous pourriez vouloir utiliser la Validation de Scalaz (qui n’a pas et ne peut pas avoir d’instance de monade) pour modéliser la validation.

    A 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 consultez par exemple la documentation de Data.Binary de Haskell :

    Le style applicatif peut parfois aboutir à un code plus rapide, car binary essaiera d’optimiser le code en regroupant les lectures.

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

    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 à quelque chose de plus ou moins comme suit:

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

    Nous soaps que maybeComputeN(whatever) ne dépend pas de s (en supposant que ce sont des méthodes bien comscopes qui ne changent pas un état mutable en arrière-plan), mais que le compilateur ne le sait pas - il peut 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 cet article de blog pour des discussions et des alternatives.)

    Le dernier point est cependant 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, par exemple dans votre méthode getPhoneByUserId , mais vous ne le faites pas souvent.

    C'est une honte que Haskell et Scala rendent actuellement le travail avec les monades beaucoup plus commode (syntaxiquement, etc.) que de travailler avec des foncteurs applicatifs, mais c'est surtout une question d'accident historique, et les développements tels que les parenthèses idiomatiques sont un pas dans la bonne direction. direction.

    Functor sert à lever des calculs dans 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 accédé à 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.