J'ai parcouru le Web à la recherche d'éclaircissements sur les suites, et il est ahurissant de voir comment la plus simple des explications peut si complètement confondre un programmeur JavaScript comme moi. Cela est particulièrement vrai lorsque la plupart des articles expliquent les suites avec du code dans Scheme ou utilisent des monades.
Maintenant que je pense enfin avoir compris l'essence des suites, je voulais savoir si ce que je sais est réellement la vérité. Si ce que je pense est vrai n'est pas réellement vrai, alors c'est de l'ignorance et non de l'illumination.
Alors, voici ce que je sais:
Dans presque tous les langages, les fonctions renvoient explicitement des valeurs (et un contrôle) à leur appelant. Par exemple:
var sum = add(2, 3);
console.log(sum);
function add(x, y) {
return x + y;
}
Maintenant, dans un langage avec des fonctions de première classe, nous pouvons passer le contrôle et renvoyer la valeur à un rappel au lieu de retourner explicitement à l'appelant:
add(2, 3, function (sum) {
console.log(sum);
});
function add(x, y, cont) {
cont(x + y);
}
Ainsi, au lieu de renvoyer une valeur à partir d'une fonction, nous continuons avec une autre fonction. Par conséquent, cette fonction est appelée une continuation de la première.
Alors, quelle est la différence entre une continuation et un rappel?
la source
Réponses:
Je crois que les continuations sont un cas particulier de rappels. Une fonction peut rappeler n'importe quel nombre de fonctions, n'importe quel nombre de fois. Par exemple:
Cependant, si une fonction rappelle une autre fonction en dernier lieu, alors la deuxième fonction est appelée une continuation de la première. Par exemple:
Si une fonction appelle une autre fonction en dernier lieu, elle s'appelle un appel de queue. Certains langages comme Scheme effectuent des optimisations des appels de fin. Cela signifie que l'appel de fin n'entraîne pas la surcharge totale d'un appel de fonction. Au lieu de cela, il est implémenté comme un simple goto (avec le cadre de pile de la fonction appelante remplacé par le cadre de pile de l'appel de queue).
Bonus : passez au style de passe de continuation. Considérez le programme suivant:
Maintenant, si chaque opération (y compris l'addition, la multiplication, etc.) était écrite sous forme de fonctions, alors nous aurions:
De plus, si nous n'étions pas autorisés à renvoyer des valeurs, nous devrons utiliser les continuations comme suit:
Ce style de programmation dans lequel vous n'êtes pas autorisé à renvoyer des valeurs (et par conséquent, vous devez recourir à la transmission de continuations) est appelé style de passage de continuation.
Il y a cependant deux problèmes avec le style de passage de continuation:
Le premier problème peut être facilement résolu en JavaScript en appelant les continuations de manière asynchrone. En appelant la continuation de manière asynchrone, la fonction retourne avant que la continuation ne soit appelée. Par conséquent, la taille de la pile d'appels n'augmente pas:
Le deuxième problème est généralement résolu en utilisant une fonction appelée
call-with-current-continuation
qui est souvent abrégée encallcc
. Malheureusement,callcc
ne peut pas être entièrement implémenté en JavaScript, mais nous pourrions écrire une fonction de remplacement pour la plupart de ses cas d'utilisation:La
callcc
fonction prend une fonctionf
et l'applique aucurrent-continuation
(abrégé encc
). Lacurrent-continuation
est une fonction de continuation qui enveloppe le reste du corps de la fonction après l'appel àcallcc
.Considérez le corps de la fonction
pythagoras
:Le
current-continuation
de la secondecallcc
est:De même, le
current-continuation
premiercallcc
est:Puisque le
current-continuation
du premier encallcc
contient un autre,callcc
il doit être converti en style de passage de continuation:Donc, essentiellement,
callcc
convertit logiquement tout le corps de la fonction en ce que nous avons commencé (et donne le nom à ces fonctions anonymescc
). La fonction pythagoras utilisant cette implémentation de callcc devient alors:Encore une fois, vous ne pouvez pas implémenter
callcc
en JavaScript, mais vous pouvez l'implémenter le style de passage de continuation en JavaScript comme suit:La fonction
callcc
peut être utilisée pour implémenter des structures de flux de contrôle complexes telles que des blocs try-catch, des coroutines, des générateurs, des fibres , etc.la source
Malgré la merveilleuse rédaction, je pense que vous confondez un peu votre terminologie. Par exemple, vous avez raison de dire qu'un appel de queue se produit lorsque l'appel est la dernière chose qu'une fonction a besoin d'exécuter, mais en ce qui concerne les continuations, un appel de queue signifie que la fonction ne modifie pas la continuation avec laquelle elle est appelée, seulement qu'elle met à jour la valeur passée à la suite (si elle le souhaite). C'est pourquoi la conversion d'une fonction récursive de queue en CPS est si simple (il vous suffit d'ajouter la continuation en tant que paramètre et d'appeler la continuation sur le résultat).
Il est également un peu étrange d'appeler les continuations un cas particulier de rappels. Je peux voir comment ils sont facilement regroupés, mais les suites ne découlent pas de la nécessité de les distinguer d'un rappel. Une suite représente en fait les instructions restantes pour terminer un calcul , ou le reste du calcul à partir de ce moment. Vous pouvez considérer une continuation comme un trou qui doit être comblé. Si je peux capturer la continuation actuelle d'un programme, alors je peux revenir exactement à la façon dont le programme était lorsque j'ai capturé la suite. (Cela facilite certainement l'écriture des débogueurs.)
Dans ce contexte, la réponse à votre question est qu'un rappel est une chose générique qui est appelée à tout moment spécifié par un contrat fourni par l'appelant [du rappel]. Un callback peut avoir autant d'arguments qu'il le souhaite et être structuré comme il le souhaite. Une continuation est donc nécessairement une procédure à un argument qui résout la valeur qui y est passée. Une continuation doit être appliquée à une valeur unique et l'application doit se produire à la fin. Lorsqu'une continuation termine l'exécution, l'expression est terminée et, selon la sémantique du langage, des effets secondaires peuvent ou non avoir été générés.
la source
La réponse courte est que la différence entre une continuation et un rappel est qu'après qu'un rappel est appelé (et s'est terminé), l'exécution reprend au point où elle a été invoquée, tandis que l'invocation d'une continuation entraîne la reprise de l'exécution au moment où la continuation a été créée. En d'autres termes: une suite ne revient jamais .
Considérez la fonction:
(J'utilise la syntaxe Javascript même si Javascript ne prend pas en charge les continuations de première classe car c'est ce dans quoi vous avez donné vos exemples, et ce sera plus compréhensible pour les personnes qui ne sont pas familières avec la syntaxe Lisp.)
Maintenant, si nous lui passons un rappel:
puis nous verrons trois alertes: "avant", "5" et "après".
D'un autre côté, si nous devions lui passer une continuation qui fait la même chose que le rappel, comme ceci:
alors nous verrions juste deux alertes: "avant" et "5". L'appel à l'
c()
intérieuradd()
met fin à l'exécutionadd()
et provoquecallcc()
le retour; la valeur renvoyée parcallcc()
était la valeur passée comme argument àc
(à savoir, la somme).En ce sens, même si invoquer une continuation ressemble à un appel de fonction, cela ressemble plus à une instruction return ou à une exception.
En fait, call / cc peut être utilisé pour ajouter des instructions de retour aux langages qui ne les prennent pas en charge. Par exemple, si JavaScript n'avait pas d'instruction return (à la place, comme beaucoup de langages Lips, renvoyant simplement la valeur de la dernière expression dans le corps de la fonction) mais avait call / cc, nous pourrions implémenter return comme ceci:
L'appel
return(i)
appelle une continuation qui met fin à l'exécution de la fonction anonyme et provoquecallcc()
le renvoi de l'indexi
dans lequel atarget
été trouvémyArray
.(NB: il y a des façons dont l'analogie du "retour" est un peu simpliste. Par exemple, si une suite s'échappe de la fonction dans laquelle elle a été créée - en étant sauvegardée quelque part dans un global, disons - il est possible que la fonction qui a créé la continuation peut renvoyer plusieurs fois même si elle n'a été invoquée qu'une seule fois .)
Call / cc peut également être utilisé pour implémenter la gestion des exceptions (throw et try / catch), des boucles et de nombreuses autres structures de contrôle.
Pour dissiper certains malentendus possibles:
L'optimisation des appels de queue n'est en aucun cas nécessaire pour prendre en charge des continuations de première classe. Considérez que même le langage C a une forme (restreinte) de continuations sous la forme de
setjmp()
, qui crée une continuation, etlongjmp()
, qui en appelle une!Il n'y a aucune raison particulière pour qu'une continuation ne prenne qu'un seul argument. C'est juste que le ou les arguments de la continuation deviennent la ou les valeurs de retour de call / cc, et call / cc est généralement défini comme ayant une seule valeur de retour, donc naturellement la continuation doit en prendre exactement une. Dans les langages prenant en charge plusieurs valeurs de retour (comme Common Lisp, Go ou même Scheme), il serait tout à fait possible d'avoir des suites acceptant plusieurs valeurs.
la source