web-dev-qa-db-fra.com

L'avenir est-il dans Scala une monade?

Pourquoi et comment spécifiquement est un Scala Future pas une monade; et quelqu'un pourrait-il le comparer à quelque chose qui est une monade, comme une option?

La raison pour laquelle je pose la question est celle de Daniel Westheide The Neophyte's Guide to Scala Part 8: Welcome to the Future où j'ai demandé si oui ou non un Scala Future était une monade, et l'auteur a répondu que non, ce qui a jeté hors de la base. Je suis venu ici pour demander des éclaircissements.

51
Nietzsche

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 les effets générés ne sont pas considérés comme faisant partie de l'équivalence sémantique (comme les messages de journalisation). Cependant, ce n'est pas ainsi que la plupart des gens les utilisent dans la pratique. Pour la plupart des gens qui utilisent des Futures efficaces (qui incluent la plupart des utilisations d'Akka et de divers cadres Web), ce ne sont tout simplement pas des monades.

Heureusement, une bibliothèque appelée Scalaz fournit une abstraction appelée Task qui n'a aucun problème avec ou sans effets.

Une définition monade

Voyons brièvement 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é gauche : bind(unit(a))(f) ≡ f(a)
  • Identité correcte : bind(m) { unit(_) } ≡ m
  • Associativité : bind(bind(m)(f))(g) ≡ bind(m) { x => bind(f(x))(g) }

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

Il existe d'autres façons de définir une monade plus ou moins identiques. Celui-ci est populaire.

Les effets conduisent à des non-valeurs

Presque chaque utilisation de Future que j'ai vue l'utilise pour des effets asychrones, des entrées/sorties avec un système externe comme un service Web ou une base de données. Lorsque nous faisons cela, un avenir n'est même pas une valeur, et des termes mathématiques comme les monades ne décrivent que des valeurs.

Ce problème se pose car Futures s'exécute immédiatement lors de la construction des données. Cela gâche la possibilité de substituer des expressions à leurs valeurs évaluées (que certains appellent la "transparence référentielle"). C'est une façon de comprendre pourquoi Scala's Futures est inadéquat pour une programmation fonctionnelle avec des 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" en appelant twoEffects:

scala> twoEffects
hello
hello

scala> twoEffects
hello
hello

Mais si les Futures étaient des valeurs, nous devrions être capables 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 met en cache le résultat, donc l'effet n'est pas exécuté la deuxième fois que nous appelons twoEffects.

Avec Futures, on finit par devoir réfléchir à la politique d'évaluation de la langue. Par exemple, dans l'exemple ci-dessus, le fait que j'utilise une valeur paresseuse plutôt qu'une valeur stricte fait une différence dans la sémantique opérationnelle. C'est exactement le genre de raisonnement tordu que la programmation fonctionnelle est conçue pour éviter - et elle le fait en programmant avec des valeurs.

Sans substitution, les lois enfreignent

Sous prétexte d'effets, les lois de la monade se brisent. Superficiellement, les lois semblent s'appliquer aux cas simples, mais au moment où nous commençons à remplacer les expressions par 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 est pas même faux parce que ce ne sont même pas des valeurs.

Pour voir comment les lois monades se brisent, il suffit de prendre en compte votre avenir 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 s'exécutera qu'une seule fois, mais vous en aurez besoin pour exécuter deux fois - une fois pour le côté droit de la loi, et une autre pour la gauche. Je vais illustrer le problème de la bonne loi sur l'identité:

scala> effect  // RHS has effect
hello

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

Le ExecutionContext implicite

Sans mettre un ExecutionContext dans une portée implicite, nous ne pouvons pas définir unit ou bind dans notre monade. En effet, l'API Scala for Futures a la signature suivante:

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]
}

Par "commodité" pour l'utilisateur, la bibliothèque standard encourage les utilisateurs à définir un contexte d'exécution dans une portée implicite, mais je pense que c'est un énorme trou dans l'API qui ne fait que conduire à des défauts. Une étendue du calcul peut avoir un contexte d'exécution défini tandis qu'une autre étendue peut avoir un autre contexte défini.

Vous pouvez peut-être ignorer le problème si vous définissez une instance de unit et bind qui épingle les deux opérations dans 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 Futures avec des compréhensions pour le rendement qui deviennent des appels map et flatMap. Pour que les compréhensions for-yield fonctionnent, un contexte d'exécution doit être défini sur une portée implicite non globale (car for-yield ne fournit pas un moyen de spécifier des paramètres supplémentaires pour les map et flatMap appels).

Pour être clair, Scala vous permet d'utiliser beaucoup de choses avec des compréhensions for-yield qui ne sont pas réellement des monades, alors ne croyez pas que vous avez une monade juste parce qu'elle fonctionne avec for-yield syntaxe.

Une meilleure façon

Il y a une belle bibliothèque pour Scala appelée Scalaz qui a une abstraction appelée scalaz.concurrent.Task. Cette abstraction n'a pas d'effets sur la construction des données comme la bibliothèque standard Future En outre, la tâche est en fait une monade. Nous composons la tâche monadiquement (nous pouvons utiliser des compréhensions pour le rendement si nous le souhaitons), et aucun effet ne s'exécute pendant la composition. Nous avons notre programme final lorsque nous avons composé une seule expression évaluant à 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âche 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 "bonjour" en appelant twoEffects:

scala> twoEffects.run
hello
hello

Et si l'on tient compte de 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 nous utilisions une valeur paresseuse ou une valeur stricte avec Task; nous obtenons bonjour imprimé deux fois de toute façon.

Si vous voulez programmer fonctionnellement, pensez à 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 "dangereuses" exécutent la tâche, exposant tous les effets internes comme des effets secondaires. Essayez donc de n'appeler aucune de ces fonctions "dangereuses" jusqu'à ce que vous ayez composé une tâche géante pour l'ensemble de votre programme.

92
Sukant Hajra

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)

Compte tenu des 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 sur les performances). Vérifier.

  3. L'associativité m.flatMap(f).flatMap(g) est équivalente à m.flatMap(x => f(x).flatMap(g)). Vérifier.

Réfutation de "Sans substitution, les lois enfreignent"

Le sens 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é dans 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, il aurait eu le même problè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 impatience soit pertinent.

4
Thayne

Comme les autres commentateurs l'ont suggéré, vous vous trompez. Future type de Scala has 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 la syntaxe de compréhension for- avec les futurs dans Scala.

2
0__