web-dev-qa-db-fra.com

Que sont Scala continuations et pourquoi les utiliser?

Je viens de terminer Programmation dans Scala, et j'ai étudié les changements entre Scala 2.7 et 2.8. Celui qui semble être le plus important est le plugin continuations, mais je ne comprends pas à quoi il sert ni comment il fonctionne. J'ai vu que c'était bon pour les E/S asynchrones, mais je n'ai pas pu savoir pourquoi. Certaines des ressources les plus populaires sur le sujet sont les suivantes:

Et cette question sur Stack Overflow:

Malheureusement, aucune de ces références n'essaye de définir à quoi servent les continuations ou ce que les fonctions shift/reset sont censées faire, et je n'ai trouvé aucune référence qui le fasse. Je n'ai pas été en mesure de deviner comment fonctionnent les exemples des articles liés (ou ce qu'ils font), donc une façon de m'aider pourrait être de parcourir ligne par ligne un de ces exemples. Même cette simple du troisième article:

reset {
    ...
    shift { k: (Int=>Int) =>  // The continuation k will be the '_ + 1' below.
        k(7)
    } + 1
}
// Result: 8

Pourquoi le résultat est-il 8? Cela m'aiderait probablement à commencer.

84
Dave

Mon blog explique ce que reset et shift font, donc vous voudrez peut-être relire cela.

Une autre bonne source, que je signale également dans mon blog, est l'entrée Wikipedia sur style de passage de continuation . Celui-ci est, de loin, le plus clair sur le sujet, bien qu'il n'utilise pas la syntaxe Scala, et la suite est explicitement passée.

L'article sur les suites délimitées, auquel je renvoie dans mon blog mais qui semble avoir été rompu, donne de nombreux exemples d'utilisation.

Mais je pense que le meilleur exemple du concept de continuations délimitées est Scala Swarm. Dans ce document, la bibliothèque arrête l'exécution de votre code à un moment donné, et le calcul restant devient la suite. La bibliothèque fait alors quelque chose - dans ce cas, en transférant le calcul à un autre Hôte et renvoie le résultat (la valeur de la variable à laquelle vous avez accédé) au calcul qui a été arrêté.

Maintenant, vous ne comprenez même pas l'exemple simple de la page Scala, alors lisez mon blog. je suis seulement soucieux d'expliquer ces bases, pourquoi le résultat est 8.

37
Daniel C. Sobral

J'ai trouvé que les explications existantes étaient moins efficaces pour expliquer le concept que je ne l'espérais. J'espère que celui-ci est clair (et correct.) Je n'ai pas encore utilisé de suites.

Lorsqu'une fonction de continuation cf est appelée:

  1. L'exécution ignore le reste du bloc shift et recommence à la fin de celui-ci
    • le paramètre passé à cf est ce que le bloc shift "évalue" à mesure que l'exécution se poursuit. cela peut être différent pour chaque appel à cf
  2. L'exécution se poursuit jusqu'à la fin du bloc reset (ou jusqu'à un appel à reset s'il n'y a pas de bloc)
    • le résultat du bloc reset (ou le paramètre de reset () s'il n'y a pas de bloc) est ce que cf renvoie
  3. L'exécution continue après cf jusqu'à la fin du bloc shift
  4. L'exécution saute jusqu'à la fin du bloc reset (ou un appel pour réinitialiser?)

Donc, dans cet exemple, suivez les lettres de A à Z

reset {
  // A
  shift { cf: (Int=>Int) =>
    // B
    val eleven = cf(10)
    // E
    println(eleven)
    val oneHundredOne = cf(100)
    // H
    println(oneHundredOne)
    oneHundredOne
  }
  // C execution continues here with the 10 as the context
  // F execution continues here with 100
  + 1
  // D 10.+(1) has been executed - 11 is returned from cf which gets assigned to eleven
  // G 100.+(1) has been executed and 101 is returned and assigned to oneHundredOne
}
// I

Cela imprime:

11
101
31
Alex Neth

Étant donné l'exemple canonique du document de recherche pour les suites délimitées de Scala, légèrement modifié de sorte que la fonction entrée dans shift porte le nom f et n'est donc plus anonyme.

def f(k: Int => Int): Int = k(k(k(7)))
reset(
  shift(f) + 1   // replace from here down with `f(k)` and move to `k`
) * 2

Le plugin Scala transforme cet exemple de telle sorte que le calcul (dans l'argument d'entrée de reset) à partir de chaque shift jusqu'à l'invocation de reset est remplacé par la fonction (par exemple f) entrée dans shift.

Le calcul remplacé est décalé (c'est-à-dire déplacé) dans une fonction k. La fonction f entre la fonction k, où k contient le calcul remplacé, k entre x: Int, et le calcul dans k remplace shift(f) par x.

f(k) * 2
def k(x: Int): Int = x + 1

Qui a le même effet que:

k(k(k(7))) * 2
def k(x: Int): Int = x + 1

Notez que le type Int du paramètre d'entrée x (c'est-à-dire la signature de type de k) a été donné par la signature de type du paramètre d'entrée de f.

Un autre exemple emprunté avec l'abstraction conceptuellement équivalente, c'est-à-dire read est la fonction entrée dans shift:

def read(callback: Byte => Unit): Unit = myCallback = callback
reset {
  val byte = "byte"

  val byte1 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "1 = " + byte1)
  val byte2 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "2 = " + byte2)
}

Je pense que cela se traduirait par l'équivalent logique de:

val byte = "byte"

read(callback)
def callback(x: Byte): Unit {
  val byte1 = x
  println(byte + "1 = " + byte1)
  read(callback2)
  def callback2(x: Byte): Unit {
    val byte2 = x
    println(byte + "2 = " + byte1)
  }
}

J'espère que cela élucidera l'abstraction commune cohérente qui a été quelque peu obscurcie par la présentation préalable de ces deux exemples. Par exemple, le premier exemple canonique a été présenté dans le document de recherche comme une fonction anonyme, au lieu de mon nom f, il n'était donc pas immédiatement clair pour certains lecteurs qu'il était abstraitement analogue au read dans le emprunté deuxième exemple.

Les suites ainsi délimitées créent l'illusion d'une inversion de contrôle de "vous m'appelez de l'extérieur de reset" à "je vous appelle à l'intérieur de reset".

Notez que le type de retour de f est, mais k ne l'est pas, doit être le même que le type de retour de reset, c'est-à-dire que f a la liberté pour déclarer tout type de retour pour k tant que f renvoie le même type que reset. Idem pour read et capture (voir aussi ENV ci-dessous).


Les continuations délimitées n'inversent pas implicitement le contrôle de l'état, par ex. read et callback ne sont pas des fonctions pures. Ainsi, l'appelant ne peut pas créer d'expressions référentiellement transparentes et n'a donc pas contrôle déclaratif (a.k.a. transparent) sur la sémantique impérative prévue .

Nous pouvons explicitement réaliser des fonctions pures avec des continuations délimitées.

def aread(env: ENV): Tuple2[Byte,ENV] {
  def read(callback: Tuple2[Byte,ENV] => ENV): ENV = env.myCallback(callback)
  shift(read)
}
def pure(val env: ENV): ENV {
  reset {
    val (byte1, env) = aread(env)
    val env = env.println("byte1 = " + byte1)
    val (byte2, env) = aread(env)
    val env = env.println("byte2 = " + byte2)
  }
}

Je pense que cela se traduirait par l'équivalent logique de:

def read(callback: Tuple2[Byte,ENV] => ENV, env: ENV): ENV =
  env.myCallback(callback)
def pure(val env: ENV): ENV {
  read(callback,env)
  def callback(x: Tuple2[Byte,ENV]): ENV {
    val (byte1, env) = x
    val env = env.println("byte1 = " + byte1)
    read(callback2,env)
    def callback2(x: Tuple2[Byte,ENV]): ENV {
      val (byte2, env) = x
      val env = env.println("byte2 = " + byte2)
    }
  }
}

Cela devient bruyant, en raison de l'environnement explicite.

Remarquez tangentiellement, Scala n'a pas l'inférence de type globale de Haskell et donc pour autant que je sache ne peut pas supporter le levage implicite vers unit d'une monade d'état (comme une stratégie possible pour cacher) l'environnement explicite), parce que l'inférence de type globale de Haskell (Hindley-Milner) dépend de ne prenant pas en charge l'héritage virtuel multiple de diamant .

9
Shelby Moore III

La suite capture l'état d'un calcul, à invoquer ultérieurement.

Pensez au calcul entre quitter l'expression shift et laisser l'expression reset comme une fonction. À l'intérieur de l'expression de décalage, cette fonction est appelée k, c'est la continuation. Vous pouvez le faire circuler, l'invoquer plus tard, même plus d'une fois.

Je pense que la valeur renvoyée par l'expression reset est la valeur de l'expression à l'intérieur de l'expression shift après le =>, mais à ce sujet, je ne suis pas sûr.

Ainsi, avec les continuations, vous pouvez envelopper un morceau de code plutôt arbitraire et non local dans une fonction. Cela peut être utilisé pour implémenter un flux de contrôle non standard, tel que le coroutining ou le backtracking.

Les continuations doivent donc être utilisées au niveau du système. Les saupoudrer à travers votre code d'application serait une recette sûre pour les cauchemars, bien pire que le pire code de spaghetti utilisant goto ne pourrait jamais l'être.

Disclaimer: Je n'ai aucune compréhension approfondie des suites dans Scala, je l'ai juste déduit de la lecture des exemples et de la connaissance des suites de Scheme.

8
starblue

De mon point de vue, la meilleure explication a été donnée ici: http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html

Un des exemples:

Pour voir le flux de contrôle un peu plus clairement, vous pouvez exécuter cet extrait de code:

reset {
    println("A")
    shift { k1: (Unit=>Unit) =>
        println("B")
        k1()
        println("C")
    }
    println("D")
    shift { k2: (Unit=>Unit) =>
        println("E")
        k2()
        println("F")
    }
    println("G")
}

Voici la sortie produite par le code ci-dessus:

A
B
D
E
G
F
C
4
Dmitry Bespalov

Un autre article (plus récent - mai 2016) sur Scala suites est:
" Voyage dans le temps à Scala: CPS en Scala (suite de scala) " par Shivansh Srivastava (shiv4nsh) .
Il fait également référence à Jim McBeath 's article mentionné dans Dmitry Bespalov 's réponse .

Mais avant cela, il décrit les continuations comme suit:

Une continuation est une représentation abstraite de l'état de contrôle d'un programme informatique .
Ce que cela signifie en fait, c'est qu'il s'agit d'une structure de données qui représente le processus de calcul à un moment donné de l'exécution du processus; la structure de données créée est accessible par le langage de programmation, au lieu d'être cachée dans l'environnement d'exécution.

Pour l'expliquer davantage, nous pouvons avoir l'un des exemples les plus classiques,

Dites que vous êtes dans la cuisine devant le réfrigérateur, en pensant à un sandwich. Vous prenez une continuation juste là et le collez dans votre poche.
Ensuite, vous sortez de la dinde et du pain du réfrigérateur et vous vous faites un sandwich qui est maintenant posé sur le comptoir.
Vous invoquez la suite dans votre poche, et vous vous retrouvez à nouveau devant le réfrigérateur, en pensant à un sandwich. Mais heureusement, il y a un sandwich sur le comptoir, et tous les matériaux utilisés pour le fabriquer ont disparu. Alors tu le manges. :-)

Dans cette description, le sandwich fait partie des données du programme (par exemple, un objet sur le tas), et plutôt que d'appeler un "make sandwich "Puis de retour, la personne a appelé un" make sandwich with current continuation ”, Qui crée le sandwich et continue là où l'exécution s'est arrêtée.

Cela étant dit, comme annoncé en avril 2014 pour Scala 2.11.0-RC1

Nous recherchons des mainteneurs pour prendre en charge les modules suivants: scala-swing , scala-continuations .
2.12 ne les inclura pas si aucun nouveau responsable n'est trouvé .
Nous continuerons probablement à maintenir les autres modules (scala-xml, scala-parser-combinators), mais l'aide est toujours grandement appréciée.

1
VonC