Est-ce que l’avenir dans Scala est une monade?

Pourquoi et comment est spécifiquement un Scala Future pas une Monade? Et est-ce que quelqu’un pourrait le comparer à quelque chose qui est une Monade, comme une Option?

La raison pour laquelle je pose la question est le Guide du Néophyte de Scala, 8ème partie: Bienvenue dans le futur, de Daniel Westheide, où j’ai demandé si un Scala Future était une Monade. Je suis venu ici pour demander une clarification.

Un résumé d’abord

Les futurs peuvent être considérés comme des monades si vous ne les construisez jamais avec des blocs efficaces (calcul pur, en mémoire), ou si des effets générés ne sont pas considérés comme faisant partie de l’équivalence sémantique (comme la journalisation des messages). Cependant, ce n’est pas ainsi que la plupart des gens les utilisent dans la pratique. Pour la plupart des utilisateurs de Futures efficaces (qui incluent la plupart des utilisations d’Akka et de différents frameworks Web), ils ne sont tout simplement pas des monades.

Heureusement, une bibliothèque appelée Scalaz fournit une abstraction appelée Task qui ne présente aucun problème avec ou sans effets.

Une définition de monade

Passons brièvement en revue ce qu’est une monade. Une monade doit pouvoir définir au moins ces deux fonctions:

 def unit[A](block: => A) : Future[A] def bind[A, B](fa: Future[A])(f: A => Future[B]) : Future[B] 

Et ces fonctions doivent satisfaire trois lois:

  • Identité de gauche : bind(unit(a))(f) ≡ f(a)
  • Identité bind(m) { unit(_) } ≡ m : bind(m) { unit(_) } ≡ m
  • Associativité : bind(bind(m)(f))(g) ≡ bind(m) { x => bind(f(x))(g) }

Ces lois doivent tenir pour toutes les valeurs possibles par définition d’une monade. S’ils ne le font pas, alors nous n’avons simplement pas de monade.

Il existe d’autres moyens de définir une monade plus ou moins identique. Celui-ci est populaire.

Les effets entraînent des non-valeurs

Presque chaque usage de Future que j’ai vu l’utilise pour des effets asynchrones, des entrées / sorties avec un système externe tel qu’un service Web ou une firebase database. Lorsque nous faisons cela, un futur n’est même pas une valeur, et les termes mathématiques comme les monades ne décrivent que des valeurs.

Ce problème se pose car les contrats à terme sont exécutés immédiatement après la construction des données. Cela gâche la possibilité de substituer des expressions à leurs valeurs évaluées (ce que certaines personnes appellent “transparence référentielle”). C’est une façon de comprendre pourquoi les Futures de Scala sont inadéquates pour la functional programming avec effets.

Voici une illustration du problème. Si nous avons deux effets:

 import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits._ def twoEffects = ( Future { println("hello") }, Future { println("hello") } ) 

nous aurons deux impressions de “bonjour” lors de l’appel de deux twoEffects :

 scala> twoEffects hello hello scala> twoEffects hello hello 

Mais si Futures étaient des valeurs, nous devrions être en mesure de factoriser l’expression commune:

 lazy val anEffect = Future { println("hello") } def twoEffects = (anEffect, anEffect) 

Mais cela ne nous donne pas le même effet:

 scala> twoEffects hello scala> twoEffects 

Le premier appel à twoEffects exécute l’effet et place le résultat en twoEffects cache. L’effet n’est donc pas exécuté la deuxième fois que nous appelons deux twoEffects .

Avec Futures, nous devons réfléchir à la politique d’évaluation de la langue. Par exemple, dans l’exemple ci-dessus, le fait d’utiliser une valeur paresseuse plutôt que ssortingcte fait une différence dans la sémantique opérationnelle. C’est exactement le genre de raisonnement tordu que la functional programming est conçue pour éviter – et elle le fait en programmant avec des valeurs.

Sans substitution, les lois se cassent

En présence d’effets, les lois de la monade sont rompues. Superficiellement, les lois semblent tenir pour des cas simples, mais dès que nous commençons à substituer des expressions à leurs valeurs évaluées, nous nous retrouvons avec les mêmes problèmes que ceux illustrés ci-dessus. Nous ne pouvons tout simplement pas parler de concepts mathématiques comme les monades lorsque nous n’avons pas de valeurs en premier lieu.

Pour le dire franchement, si vous utilisez des effets avec vos Futures, dire que ce sont des monades n’est même pas faux, car elles ne sont même pas des valeurs.

Pour voir comment les lois monades se brisent, il vous suffit de prendre en compte votre futur efficace:

 import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits._ def unit[A] (block: => A) : Future[A] = Future(block) def bind[A, B] (fa: Future[A]) (f: A => Future[B]) : Future[B] = fa flatMap f lazy val effect = Future { println("hello") } 

Encore une fois, il ne sera exécuté qu’une seule fois, mais vous en aurez besoin deux fois – une fois pour la partie droite de la loi et une autre pour la gauche. Je vais illustrer le problème pour la bonne loi sur l’identité:

 scala> effect // RHS has effect hello scala> bind(effect) { unit(_) } // LHS doesn't 

L’exécutionContext implicite

Sans placer un object ExecutionContext dans la scope implicite, nous ne pouvons définir ni unit ni bind dans notre monade. C’est parce que l’API Scala pour Futures a ces signatures:

 object Future { // what we need to define unit def apply[T] (body: ⇒ T) (implicit executor: ExecutionContext) : Future[T] } trait Future { // what we need to define bind flatMap[S] (f: T ⇒ Future[S]) (implicit executor: ExecutionContext) : Future[S] } 

En tant que “commodité” pour l’utilisateur, la bibliothèque standard encourage les utilisateurs à définir un contexte d’exécution dans une scope implicite, mais je pense que c’est un énorme trou dans l’API qui ne conduit qu’à des défauts. Une scope du calcul peut avoir un contexte d’exécution défini alors qu’une autre peut avoir un autre contexte défini.

Vous pouvez peut-être ignorer le problème si vous définissez une instance d’ unit et une bind qui associe les deux opérations à un seul contexte et utilisez cette instance de manière cohérente. Mais ce n’est pas ce que les gens font la plupart du temps. La plupart du temps, les gens utilisent les contrats à terme avec des compréhensions à rendement qui deviennent des appels sur map et sur flatMap . Pour que les compréhensions de rendement puissent fonctionner, un contexte d’exécution doit être défini sur une étendue implicite non globale (car for-yield ne permet pas de spécifier des parameters supplémentaires aux appels map et flatMap ).

Pour être clair, Scala vous permet d’utiliser beaucoup de choses avec des compréhensions de rendement qui ne sont pas réellement des monades, alors ne croyez pas que vous ayez une monade simplement parce qu’elle fonctionne avec une syntaxe à rendement élevé.

Une meilleure façon

Il existe une belle bibliothèque pour Scala appelée Scalaz qui a une abstraction appelée scalaz.concurrent.Task. Cette abstraction ne produit aucun effet sur la construction des données, contrairement à la bibliothèque standard Future. De plus, Task est en réalité une monade. Nous composons la tâche de manière monadique (nous pouvons utiliser des compréhensions de rendement si nous le souhaitons), et aucun effet ne s’exécute pendant que nous composons. Nous avons notre programme final lorsque nous avons composé une seule expression évaluant la Task[Unit] . Cela finit par être notre équivalent d’une fonction “principale”, et nous pouvons enfin l’exécuter.

Voici un exemple illustrant comment nous pouvons remplacer les expressions de tâches par leurs valeurs évaluées respectives:

 import scalaz.concurrent.Task import scalaz.IList import scalaz.syntax.traverse._ def twoEffects = IList( Task delay { println("hello") }, Task delay { println("hello") }).sequence_ 

Nous aurons deux impressions de “hello” en appelant les deux twoEffects :

 scala> twoEffects.run hello hello 

Et si nous éliminons l’effet commun,

 lazy val anEffect = Task delay { println("hello") } def twoEffects = IList(anEffect, anEffect).sequence_ 

nous obtenons ce que nous attendons:

 scala> twoEffects.run hello hello 

En fait, peu importe que l’on utilise une valeur paresseuse ou une valeur ssortingcte avec Task; nous obtenons bonjour imprimé deux fois dans les deux sens.

Si vous voulez programmer fonctionnellement, envisagez d’utiliser Task partout où vous pouvez utiliser Futures. Si une API vous impose des contrats à terme, vous pouvez convertir le futur en tâche:

 import concurrent. { ExecutionContext, Future, Promise } import util.Try import scalaz.\/ import scalaz.concurrent.Task def fromScalaDeferred[A] (future: => Future[A]) (ec: ExecutionContext) : Task[A] = Task .delay { unsafeFromScala(future)(ec) } .flatMap(identity) def unsafeToScala[A] (task: Task[A]) : Future[A] = { val p = Promise[A] task.runAsync { res => res.fold(p failure _, p success _) } p.future } private def unsafeFromScala[A] (future: Future[A]) (ec: ExecutionContext) : Task[A] = Task.async( handlerConversion .andThen { future.onComplete(_)(ec) }) private def handlerConversion[A] : ((Throwable \/ A) => Unit) => Try[A] => Unit = callback => { t: Try[A] => \/ fromTryCatch t.get } .andThen(callback) 

Les fonctions “non sécurisées” exécutent la tâche en exposant les effets internes comme des effets secondaires. Donc, essayez de ne pas appeler l’une de ces fonctions “dangereuses” avant d’avoir composé une tâche géante pour l’ensemble de votre programme.

Comme les autres commentateurs l’ont suggéré, vous vous trompez. Le type Future de Scala a les propriétés monadiques:

 import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits._ def unit[A](block: => A): Future[A] = Future(block) def bind[A, B](fut: Future[A])(fun: A => Future[B]): Future[B] = fut.flatMap(fun) 

C’est pourquoi vous pouvez utiliser for syntaxe de compréhension avec les futurs dans Scala.

Je crois qu’un avenir est une monade, avec les définitions suivantes:

 def unit[A](x: A): Future[A] = Future.successful(x) def bind[A, B](m: Future[A])(fun: A => Future[B]): Future[B] = fut.flatMap(fun) 

Considérant les trois lois:

  1. Identité de gauche:

    Future.successful(a).flatMap(f) est équivalent à f(a) . Vérifier.

  2. Bonne identité:

    m.flatMap(Future.successful _) est équivalent à m (moins quelques implications possibles en m.flatMap(Future.successful _) performances). Vérifier.

  3. Associativity m.flatMap(f).flatMap(g) est équivalent à m.flatMap(x => f(x).flatMap(g)) . Vérifier.

Réfutation à “Sans substitution, les lois cèdent”

La signification de l’équivalent dans les lois de la monade, si je comprends bien, est que vous pouvez remplacer un côté de l’expression par l’autre côté de votre code sans changer le comportement du programme. En supposant que vous utilisez toujours le même contexte d’exécution, je pense que c’est le cas. Dans l’exemple donné par @sukant, le problème aurait été le même s’il avait utilisé Option au lieu de Future . Je ne pense pas que le fait que les contrats à terme soient évalués avec empressement soit pertinent.