Como implementar uma linguagem de programação em JavaScript. Parte 3: Intérprete de CPS

Olá Apresento a você a terceira parte da minha tradução do guia para implementar minha linguagem de programação JavaScript - PL Tutorial .


Do tradutor


Criaremos nossa própria linguagem de programação - linguagem λ (no original - linguagem λ ). No processo de criação, usaremos muitas técnicas interessantes, como descida recursiva, estilo de transferência de controle e técnicas básicas de otimização. Serão criadas duas versões do intérprete - o regular e o CPS, o transcompilador em JavaScript.


O autor do original é Mihai Bazon , o autor da famosa biblioteca UglifyJS (uma ferramenta para minimizar e formatar o código JS).



PS Existe um erro no interpretador e compilador: em expressões como a() && b() ou a() || b() a() || b() ambas as partes são sempre executadas. Obviamente, isso está errado porque a() falso para o operador && ou falso para o || , o valor de b() não desempenha nenhum papel. Isso não é difícil de corrigir.


Intérprete CPS


Nossa linguagem λ tem duas desvantagens:


  • A recursão é limitada à pilha JS, portanto, não temos uma maneira normal de fazer loops.
  • O intérprete é lento, então a recursão é muito lenta.

Agora, corrigiremos a primeira falha sem prestar atenção ao fato de que o intérprete se tornará ainda mais lento. Reescreveremos o intérprete no estilo "estilo de passagem de continuação" (CPS).


O que é uma "transferência de continuação"


Você faz isso no NodeJS o tempo todo:


 fs.readFile("file.txt", "utf8", function CC(error, data){ //   - "" //     'return', // 'readFile'  ,  . }); 

A cada passo, há um retorno de chamada que será chamado quando você precisar continuar. O estilo de transferência de continuação torna a transferência de controle "explícita" - você não usa return , throw , break ou continue . Não há saltos implícitos no código. Você não pode nem usar loops for ou while com funções assíncronas. Nesse caso, por que precisamos deles na linguagem de programação?


Escrever código no estilo de transmitir uma continuação é difícil e fácil de cometer erros, mas apenas reescreveremos o intérprete nesse estilo.


evaluate função


A função evaluate receberá três argumentos: expressão (AST), contexto (ambiente) e a função que será chamada quando o resultado for. Aqui está o código, para cada fragmento, há uma explicação:


 function evaluate(exp, env, callback) { switch (exp.type) { 

Para constantes, apenas retornaremos seu valor. Mas lembre-se, não temos return - apenas chamamos o retorno e passamos o valor.


  case "num": case "str": case "bool": callback(exp.value); return; 

O nó var também é simples - obtenha a variável do contexto e passe-a para a função:


  case "var": callback(env.get(exp.value)); return; 

Para assign nós, precisamos obter o valor da expressão esquerda ( right ). Para fazer isso, chamamos de evaluate , passando uma função que obterá o resultado (para o lado direito da expressão). E então chamamos o callback com o valor da variável, configurando a variável no contexto atual:


  case "assign": if (exp.left.type != "var") throw new Error("Cannot assign to " + JSON.stringify(exp.left)); evaluate(exp.right, env, function(right){ callback(env.set(exp.left.value, right)); }); return; 

Quase o mesmo para nós do tipo binary , mas aqui precisamos primeiro obter o valor do campo left e, somente então, o valor do campo right . Depois, chamamos o retorno de chamada, passando o resultado da operação:


  case "binary": evaluate(exp.left, env, function(left){ evaluate(exp.right, env, function(right){ callback(apply_op(exp.operator, left, right)); }); }); return; 

O nó let parece mais complicado, mas na verdade é simples. Temos algum número de variáveis. O campo def (valor inicial) pode estar vazio; nesse caso, o definiremos como false . Mas se houver um valor, precisamos chamar evaluate recursivamente para obtê-lo.


Se você já trabalhou com o NodeJS antes, já fez isso várias vezes. Devido aos retornos de chamada, não podemos usar, portanto, devemos interpretar essas expressões uma por vez (imagine a função de evaluate como assíncrona). A função de loop abaixo (chamada imediatamente) obtém o contexto e o número da definição que precisa ser processada:


  • Se esse número for igual ao número de variáveis ​​( vars.length ), isso significa que já definimos todas as expressões para poder executar o corpo da expressão. Observe que desta vez não ligamos para callback , mas passamos para evaluate , que será chamado.
  • Se esse número for menor, será necessário calcular a definição atual e passar uma função que chamará loop(scope, i + 1) , antes de copiar o contexto.
      case "let": (function loop(env, i){ if (i < exp.vars.length) { var v = exp.vars[i]; if (v.def) evaluate(v.def, env, function(value){ var scope = env.extend(); scope.def(v.name, value); loop(scope, i + 1); }); else { var scope = env.extend(); scope.def(v.name, false); loop(scope, i + 1); } } else { evaluate(exp.body, env, callback); } })(env, 0); return; 

O nó lambda será processado em uma função separada, como antes:


  case "lambda": callback(make_lambda(env, exp)); return; 

Para interpretar if , primeiro interpretamos a condição. Se não for falso, interpretaremos a expressão then , em outro caso, interpretaremos else se houver um ou retornaremos false se não houver. Como antes, por enquanto e apenas passamos o callback como a "ação a ser executada após a execução" da expressão:


  case "if": evaluate(exp.cond, env, function(cond){ if (cond !== false) evaluate(exp.then, env, callback); else if (exp.else) evaluate(exp.else, env, callback); else callback(false); }); return; 

O nó prog é interpretado de maneira semelhante ao nó let , mas com a diferença de que não precisamos copiar o contexto ou definir variáveis. E, novamente, temos uma função de loop que recebe um número de expressão. Quando é igual a prog.length , concluímos todas as expressões e simplesmente retornamos o resultado da última expressão (com a palavra "return", quero dizer que chamamos de callback chamada). Observe que lembramos o último valor passando-o como um argumento para a função loop (no início, é false no caso em que o corpo está vazio):


  case "prog": (function loop(last, i){ if (i < exp.prog.length) evaluate(exp.prog[i], env, function(val){ loop(val, i + 1); }); else { callback(last); } })(false, 0); return; 

Para um nó do tipo call precisamos interpretar func e, em seguida, todos os argumentos estão em ordem. E, novamente, há uma função de loop que funciona com o mesmo princípio que let ou prog , com a diferença de que agora cria uma matriz como resultado:


  case "call": evaluate(exp.func, env, function(func){ (function loop(args, i){ if (i < exp.args.length) evaluate(exp.args[i], env, function(arg){ args[i + 1] = arg; loop(args, i + 1); }); else { func.apply(null, args); } })([ callback ], 0); }); return; 

Bem, o final padrão: se não sabemos o que fazer, lance uma exceção:


  default: throw new Error("I don't know how to evaluate " + exp.type); } } 

Você pode perceber que cada case acima termina com a palavra-chave return . Mas não há valor de retorno - o resultado é sempre passado para a função de callback .


Nova função make_lambda


Nesse intérprete, todas as funções receberão uma “continuação” como o primeiro argumento - a função que devemos chamar para passar o resultado. Depois são os argumentos usuais para a função. Aqui está o novo código para a função make_lambda :


 function make_lambda(env, exp) { if (exp.name) { env = env.extend(); env.def(exp.name, lambda); } function lambda(callback) { var names = exp.vars; var scope = env.extend(); for (var i = 0; i < names.length; ++i) scope.def(names[i], i + 1 < arguments.length ? arguments[i + 1] : false); evaluate(exp.body, scope, callback); } return lambda; } 

Ele não é muito diferente. Ele adiciona novas variáveis ​​a um novo contexto. Além disso, devemos considerar que o primeiro argumento é callback . Finalmente, a função de evaluate é usada para executar o código da função em um novo contexto, mas, como antes, passamos um callback .


A propósito, este é o único lugar que usei o loop for . Isso ocorre porque os valores do argumento já estão calculados quando o nó da call foi processado.


Funções nativas


Nesse intérprete, funções nativas recebem um callback como o primeiro argumento. Devemos lembrar disso quando definimos funções nativas. Aqui está o código de exemplo para o novo intérprete:


 var code = "sum = lambda(x, y) x + y; print(sum(2, 3));"; var ast = parse(TokenStream(InputStream(code))); var globalEnv = new Environment(); // define the "print" primitive function globalEnv.def("print", function(callback, txt){ console.log(txt); callback(false); // call the continuation with some return value // if we don't call it, the program would stop // abruptly after a print! }); // run the evaluator evaluate(ast, globalEnv, function(result){ // the result of the entire program is now in "result" }); 

Little test


Vamos tentar calcular os números de Fibonacci novamente:


 fib = λ(n) if n < 2 then n else fib(n - 1) + fib(n - 2); time( λ() println(fib(10)) ); 

Mas, se tentarmos encontrar o 27º número, obtemos um estouro de pilha. Em geral, a pilha agora está crescendo muito mais rápido, portanto, agora podemos contar o número de Fibonacci apenas até o dia 12 (pelo menos no meu navegador). Isso não é muito bom, então você precisa corrigi-lo de alguma forma.


Protegemos a pilha


Em um intérprete CPS, a pilha cresce muito mais rapidamente porque o intérprete sempre chama funções recursivamente, nunca retornando um resultado. Embora tenhamos return ao intérprete, precisamos deles, mas no caso de recursão muito profunda, nunca os alcançamos.


Vamos imaginar como nossa pilha procura um programa muito simples. Vou mostrar o pseudo-código e não adicionei o env porque ele não desempenha nenhum papel aqui:


 print(1 + 2 * 3); ## : evaluate( print(1 + 2 * 3), K0 ) evaluate( print, K1 ) K1(print) #  'var',      evaluate( 1 + 2 * 3, K2 ) evaluate( 2 * 3, K3 ) evaluate( 2, K4 ) K4(2) # 2  ,      evaluate( 3, K5 ) #    3,      K5(3) K3(6) #  2*3 evaluate( 1, K6 ) #  ,  K6(1) K2(7) #  1+2*3 print(K0, 7) # ,     'print' K0(false) #  . 'print'  'false'. 

Somente após a última chamada, uma longa sequência de return inútil reduz a pilha. Se usarmos tanto espaço de pilha para um programa simples, é difícil imaginar o que acontecerá com a fib(13) .


Proteção de pilha


Em nosso novo intérprete, a pilha simplesmente não é necessária. Tudo o que precisa ser feito depois que alguma expressão ocorre em um callback , que é passado como argumento. Então, aqui temos uma pergunta: e se o JavaScript tornasse possível "despejar" a pilha. Depois, podemos largar a pilha e a recursão infinitamente profunda funcionará.


Vamos imaginar que temos uma função GUARD que pode fazer isso. Ele obtém dois valores: a função a ser chamada e os argumentos que precisam ser passados. Ele verifica: se a pilha for muito profunda, ela limpará a pilha e chamará a função passada. Em outro caso, ela não faz nada.


Usando a nova função, reescrevemos o intérprete como mostrado abaixo. Não vou comentar sobre cada caso individual, existe o código descrito anteriormente, mas usando a função GUARD :


 function evaluate(exp, env, callback) { GUARD(evaluate, arguments); switch (exp.type) { case "num": case "str": case "bool": callback(exp.value); return; case "var": callback(env.get(exp.value)); return; case "assign": if (exp.left.type != "var") throw new Error("Cannot assign to " + JSON.stringify(exp.left)); evaluate(exp.right, env, function CC(right){ GUARD(CC, arguments); callback(env.set(exp.left.value, right)); }); return; case "binary": evaluate(exp.left, env, function CC(left){ GUARD(CC, arguments); evaluate(exp.right, env, function CC(right){ GUARD(CC, arguments); callback(apply_op(exp.operator, left, right)); }); }); return; case "let": (function loop(env, i){ GUARD(loop, arguments); if (i < exp.vars.length) { var v = exp.vars[i]; if (v.def) evaluate(v.def, env, function CC(value){ GUARD(CC, arguments); var scope = env.extend(); scope.def(v.name, value); loop(scope, i + 1); }); else { var scope = env.extend(); scope.def(v.name, false); loop(scope, i + 1); } } else { evaluate(exp.body, env, callback); } })(env, 0); return; case "lambda": callback(make_lambda(env, exp)); return; case "if": evaluate(exp.cond, env, function CC(cond){ GUARD(CC, arguments); if (cond !== false) evaluate(exp.then, env, callback); else if (exp.else) evaluate(exp.else, env, callback); else callback(false); }); return; case "prog": (function loop(last, i){ GUARD(loop, arguments); if (i < exp.prog.length) evaluate(exp.prog[i], env, function CC(val){ GUARD(CC, arguments); loop(val, i + 1); }); else { callback(last); } })(false, 0); return; case "call": evaluate(exp.func, env, function CC(func){ GUARD(CC, arguments); (function loop(args, i){ GUARD(loop, arguments); if (i < exp.args.length) evaluate(exp.args[i], env, function CC(arg){ GUARD(CC, arguments); args[i + 1] = arg; loop(args, i + 1); }); else { func.apply(null, args); } })([ callback ], 0); }); return; default: throw new Error("I don't know how to evaluate " + exp.type); } } 

Para funções anônimas, precisamos adicionar um nome para que possamos passá-los para a função GUARD . Eu usei o nome CC ( current continuation ). Você pode usar arguments.callee , mas esta é uma API desatualizada.


Além disso, a mesma alteração no make_lambda :


 function make_lambda(env, exp) { if (exp.name) { env = env.extend(); env.def(exp.name, lambda); } function lambda(callback) { GUARD(lambda, arguments); // <--  var names = exp.vars; var scope = env.extend(); for (var i = 0; i < names.length; ++i) scope.def(names[i], i + 1 < arguments.length ? arguments[i + 1] : false); evaluate(exp.body, scope, callback); } return lambda; } 

A implementação do GUARD muito simples. Como sair de uma chamada profunda? Usando exceções. Para fazer isso, haverá uma variável global que indicará quanto mais recursão podemos fazer. Se chegar a zero, lançamos o objeto Continuation , no qual haverá uma continuação - função e argumentos:


 var STACKLEN; function GUARD(f, args) { if (--STACKLEN < 0) throw new Continuation(f, args); } function Continuation(f, args) { this.f = f; this.args = args; } 

E no final, precisamos de um loop que capture objetos de Continuation . Devemos chamar o intérprete através deste loop para que tudo funcione:


 function Execute(f, args) { while (true) try { STACKLEN = 200; return f.apply(null, args); } catch(ex) { if (ex instanceof Continuation) f = ex.f, args = ex.args; else throw ex; } } 

A função Execute aceita a função a ser chamada e os argumentos para ela. Funciona em loop, mas preste atenção para return se a função funcionar sem excesso de pilha, paramos imediatamente. STACKLEN redefinido toda vez que iniciamos uma iteração de loop. Um valor de 200 - se encaixa bem. Quando capturamos o objeto Continuation , substituímos uma nova função e argumentos e continuamos o loop. Além disso, devido a uma exceção, a pilha é limpa, para que possamos continuar.


Para iniciar o intérprete, agora usamos Execute :


 Execute(evaluate, [ ast, globalEnv, function(result){ console.log("*** Result:", result); }]); 

Teste


A função fib agora funcionará:


 fib = λ(n) if n < 2 then n else fib(n - 1) + fib(n - 2); time( λ() println(fib(20)) ); # 6765, ~300ms 

É uma pena, mas se você tentar encontrar a fib(27) , levará cerca de quatro vezes o tempo que um intérprete comum. Mas agora temos recursão infinita:


 sum = λ(n, ret) if n == 0 then ret else sum(n - 1, ret + n); # compute 1 + 2 + ... + 50000 time( λ() println(sum(50000, 0)) ); # 1250025000, ~700ms 

Obviamente, a linguagem λ é muito mais lenta que o JavaScript. Imagine, todo acesso a uma variável passa por um objeto Environment . Não faz sentido tentar otimizar o intérprete - não obteremos um ganho significativo de desempenho. Para melhorar o desempenho, existe uma solução: compile a linguagem λ em JS, e é isso que faremos. Primeiro, vamos ver o que podemos fazer de interessante com um intérprete do CPS.


Continuação


Agora que o intérprete trabalha no estilo de transmissão de continuação, e todas as funções, funções da linguagem λ e funções JS nativas, recebem a função de continuação como o primeiro argumento a retornar o resultado (esse argumento é necessário para as funções JavaScript, embora seja invisível para as funções da linguagem λ).


O callback variável significa a continuação de todo o programa. Tudo o que o programa fará a seguir. Vamos chamar essa variável de 'continuação atual' ou k no código.


Além disso, se não causarmos uma continuação, o programa será interrompido. Não podemos fazer isso a partir da linguagem λ porque make_lambda chama continuação de qualquer maneira. Mas podemos fazer isso a partir de uma função nativa. Eu criei uma função para mostrar como isso funciona:


 globalEnv.def("halt", function(k){}); 

Esta é uma função que não faz nada. Ela recebe a continuação ( k ), mas não a chama:


 println("foo"); halt(); println("bar"); 

Conclusão:


 foo 

Se você excluir a chamada halt() , a saída será: foo / bar / ***Result: false (porque o último println retorna false ). Mas com halt() isso apenas gera foo . * Agora não há resultado, porque halt() não causa continuação e, portanto, o retorno de chamada que passamos para evaluate - aquele que exibe a string ***Result , nunca é chamado. A função chamada evaluate não percebe que o programa foi interrompido. No NodeJS, isso seria um tiro no pé.




Imagine que precisamos de uma função sleepe que interrompa um programa sem interromper o navegador (portanto, sem loops estúpidos). Podemos facilmente implementar isso usando uma função nativa:


 globalEnv.def("sleep", function(k, milliseconds){ setTimeout(function(){ Execute(k, [ false ]); //   ,  'false' }, milliseconds); }); 

Um pequeno inconveniente é que precisamos usar o Execute , pois o setTimeout causará um retorno de chamada, que acionará Continuation , e ninguém o capturará. Veja como usá-lo:


 let loop (n = 0) { if n < 10 { println(n); sleep(250); loop(n + 1); } }; println("And we're done"); 

Conclusão:


 0 1 2 3 4 5 6 7 8 9 And we're done ***Result: false 

Observe que há um pequeno atraso entre cada linha. Você pode tentar executar esse código no artigo original .


Isso já é algo que você não pode fazer no JavaScript normal, exceto pelo uso da transmissão manual de continuação (e também não é possível usar um loop for ):


 (function loop(n){ if (n < 10) { console.log(n); setTimeout(function(){ loop(n + 1); }, 250); } else { println("And we're done"); //    } })(0); 



Imagine como você pode usar a API do NodeJS em uma linguagem λ:


 globalEnv.def("readFile", function(k, filename){ fs.readFile(filename, function(err, data){ // error handling is a bit more complex, ignoring for now Execute(k, [ data ]); // hope it's clear why we need the Execute }); }); globalEnv.def("writeFile", function(k, filename, data){ fs.writeFile(filename, data, function(err){ Execute(k, [ false ]); }); }); 

Uso:


 copyFile = λ(source, dest) { writeFile(dest, readFile(source)); }; copyFile("foo.txt", "bar.txt"); 

E tudo isso funciona de forma assíncrona. Não há mais retorno de chamada.




Aqui está um exemplo mais interessante. Eu escrevi a seguinte função nativa:


 globalEnv.def("twice", function(k, a, b){ k(a); k(b); }); 

O programa usa dois argumentos b e chama continuação duas vezes, uma vez para cada argumento. Vamos tentar chamá-lo:


 println(2 + twice(3, 4)); println("Done"); 

Conclusão:


 5 Done ***Result: false 6 Done ***Result: false 

A conclusão é estranha se você nunca trabalhou com continuações anteriores, mas tente entender esse código por conta própria. Uma pequena dica: o programa inicia uma vez, mas retorna o resultado duas vezes.


Generalização: CallCC


Costumávamos brincar com o fogo quando escrevíamos funções nativas. Não há nenhuma maneira na linguagem λ para interromper a execução da continuação atual. CallCC ajudará a resolver esse problema. O nome é a abreviação da função de Scheme - call-with-current-continuation (que geralmente é chamada de chamada / cc no esquema).


 globalEnv.def("CallCC", function(k, f){ f(k, function CC(discarded, ret){ k(ret); }); }); 

Ele reifica a continuação atual em uma função que pode ser chamada diretamente da linguagem λ. Esta função ignorará sua continuação discarded original e chamará a continuação que foi passada para a função CallCC .


Usando esta função, podemos implementar (já diretamente na linguagem, não através de funções nativas!) Um grande conjunto de instruções de controle para o fluxo de execução em que nem pensávamos antes - começando pelas exceções e terminando com return . Vamos começar do último.


return implementação


 foo = λ(return){ println("foo"); return("DONE"); println("bar"); }; CallCC(foo); 

Conclusão:


 foo ***Result: DONE 

A função foo recebe um argumento que faz o mesmo que return do JavaScript (mas é chamado como uma função regular). Ignora a continuação atual (que produziria 'bar') e sai da função, retornando o valor fornecido ("DONE"). Obviamente, isso só funciona se você chamar uma função usando o CallCC para passar a continuação como return . Podemos criar um wrapper para isso:


 with-return = λ(f) λ() CallCC(f); foo = with-return(λ(return){ println("foo"); return("DONE"); println("bar"); }); foo(); 

Conclusão:


 foo ***Result: DONE 

A vantagem desse método é que não precisamos mais usar o CallCC quando chamamos foo . Seria bom, é claro, se não precisássemos aceitar return como argumento ou usar a função with-return , mas na linguagem não há como adicionar açúcar sintático diretamente a partir dele, para isso precisamos pelo menos modificar o analisador (+1 para Lisp).


Transições


Por exemplo, precisamos escrever um programa que procure todos os pares de números inteiros positivos de até 100, de modo que, se a multiplicação deles der 84. Esta não é uma tarefa difícil e você poderá imaginar imediatamente dois loops aninhados para resolvê-lo, mas aqui vamos de uma maneira diferente. Vamos criar duas funções: guess e fail . O primeiro escolhe o número e o segundo diz a ela "tente outro número". Vamos usá-los assim:


 a = guess(1); #  -  >= 1 b = guess(a); #  -  >= a if a * b == 84 { #    : print(a); print(" x "); println(b); }; fail(); #    `guess`     

:


 fail = λ() false; guess = λ(current) { CallCC(λ(k){ let (prevFail = fail) { fail = λ(){ current = current + 1; if current > 100 { fail = prevFail; fail(); } else { k(current); }; }; k(current); }; }); }; a = guess(1); b = guess(a); if a * b == 84 { print(a); print(" x "); println(b); }; fail(); 

, , a , 84 , b , 84 / a . guess : start end — . , .


try catch Common Lisp


catch throw . :


 f1 = λ() { throw("foo", "EXIT"); print("not reached"); }; println(catch("foo", λ() { f1(); print("not reached"); })); #  EXIT 

  • catch(TAG, function) , , TAG ', function .
  • throw(TAG, value) , , TAG ' , value .

:


 ##  , 'throw'   . ##       `error`, ##     JavaScript.    . throw = λ(){ println("ERROR: No more catch handlers!"); halt(); }; catch = λ(tag, func){ CallCC(λ(k){ let (rethrow = throw, ret) { ##   ,     . throw = λ(t, val) { throw = rethrow; #   ,   . if t == tag then k(val) else throw(t, val); }; ##      . ret = func(); ##       (  'throw') ##    .   . throw = rethrow; # XXX ##  . ret; }; }); }; 

Um exemplo:


 # ... f1 = λ() { throw("foo", "EXIT"); print("not reached"); }; println(catch("foo", λ() { f1(); print("not reached"); })); 


catch , , , . , , , CallCC . , . " " — — . , , , catch / throw , .


. , catch . , throw , , catch , . Por exemplo:


 exit = false; #  . x = 0; # :   ,   'exit()'  CallCC( λ(k) exit = k ); ## 'exit()'   ... if x == 0 then catch("foo", λ(){ println("in catch"); x = 1; #  exit(); }); println("After catch"); throw("foo", "FOO"); 

Conclusão:


 After catch After catch ERROR: No more catch handlers! 

'After catch' , 'ERROR: No more catch handlers!'. - , 'After catch' , . , '' , catch . , 'XXX' catch , throw , catch .


( , .)


CallCC (, , CallCC ). , — CallCC .


Yield


, , yield :


 foo = with-yield(λ(yield){ yield(1); yield(2); yield(3); "DONE"; }); println(foo()); # 1 println(foo()); # 2 println(foo()); # 3 println(foo()); # DONE 

yield , . , return . , , yield , .


:


 fib = with-yield(λ(yield){ let loop (a = 1, b = 1) { yield(b); loop(b, a + b); }; }); ##   50   let loop (i = 0) { if i < 50 { println(fib()); loop(i + 1); }; }; 

fib . . yield . , , 50 , 50 .


, , , , .



, .


yield , , . , , return . , , yield , yield , . , . :


 with-yield = λ(func) { ## with-yield     λ() { CallCC(λ(kret){ # kret     let (yield) { ##  yield yield = λ(value) { # yield  ,    CallCC(λ(kyld){ # kyld    yield... func = kyld; # ...     kret(value); #  . }); }; ## , ,  ,   yield. func(yield); }; }); }; }; 

yield , . , , , "DONE".


, . , - , , 4 :


 println(foo()); foo(); 

.


№1: yield


, , , , yield ( kyld , , ) :


  yield(2); yield(3); "DONE"; 

yield ? yield , , yield . , . , yield return , :


 with-yield = λ(func) { let (return, yield) { yield = λ(value) { CallCC(λ(kyld){ func = kyld; return(value); # 'return'  ,     }); #        . }; # λ(val) { # CallCC(λ(kret){ # return = kret; # <-  func(val || yield); }); }; }; }; 

, , yield , yield ( ). yield .


, , println(foo()) :


 with-yield = λ(func) { let (return, yield) { yield = λ(value) { CallCC(λ(kyld){ func = kyld; return(value); }); }; λ(val) { CallCC(λ(kret){ return = kret; func(val || yield); }); }; }; }; foo = with-yield(λ(yield){ yield(1); yield(2); yield(3); "DONE"; }); println(foo()); println(foo()); println(foo()); 

, . , print(foo()); foo() . , println(foo()) ? ...


№2: WTF?


. : , foo() . , ? — .


 println(foo()); ## yield 1 <-----------------  ---------------------------+ println(foo()); ## yield 2 | println(foo()); ## yield 3 | println(foo()); ##   "DONE",   foo()  -->--+ 

with-yield :


  func(val || yield); #... 

yield , , #... . , , ( "DONE" ), , "DONE" , . foo() , "DONE" . :


 println(foo()); println("bar"); println(foo()); println(foo()); foo(); 

: 1, bar, 2, 3, DONE, bar, DONE, bar, ... .


func - , . , "no more continuations" :


  val = func(val || yield); func = λ() "NO MORE CONTINUATIONS"; kret(val); 

:


 with-yield = λ(func) { let (return, yield) { yield = λ(value) { CallCC(λ(kyld){ func = kyld; return(value); }); }; λ(val) { CallCC(λ(kret){ return = kret; val = func(val || yield); func = λ() "NO MORE CONTINUATIONS"; kret(val); }); }; }; }; foo = with-yield(λ(yield){ yield(1); yield(2); yield(3); "DONE"; }); println(foo()); println(foo()); println(foo()); println(foo()); 

, :


 1 2 3 DONE NO MORE CONTINUATIONS NO MORE CONTINUATIONS NO MORE CONTINUATIONS ***Result: false 

1, 2, 3, DONE , "NO MORE CONTINUATIONS" . , :


 print("A. "); println(foo()); print("B. "); println(foo()); print("C. "); println(foo()); print("D. "); println(foo()); ##   : A. 1 B. 2 C. 3 D. DONE B. NO MORE CONTINUATIONS C. NO MORE CONTINUATIONS D. NO MORE CONTINUATIONS ***Result: false 

, : , , , , "B." .


, yield , :


 with-yield = λ(func) { let (return, yield) { yield = λ(value) { CallCC(λ(kyld){ func = kyld; return(value); }); }; λ(val) { CallCC(λ(kret){ return = kret; val = func(val || yield); func = λ() "NO MORE CONTINUATIONS"; kret(val); }); }; }; }; fib = with-yield(λ(yield){ let loop (a = 1, b = 1) { yield(b); loop(b, a + b); }; }); ##   50   let loop (i = 0) { if i < 50 { println(fib()); loop(i + 1); }; }; 

Conclusão
 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 14930352 24157817 39088169 63245986 102334155 165580141 267914296 433494437 701408733 1134903170 1836311903 2971215073 4807526976 7778742049 12586269025 20365011074 ***Result: false 

, (, ), " ".


: reset shift


yield : reset shift . " " — , . reset , shift , CallCC .


, reset shift — . reset , shift , reset .


, with-yield :


 with-yield = λ(func) { let (yield) { ## 'yield'  'shift'     ##  'reset'.  ,   ,    ##   'func' — ,   `func()` ##    ,    . yield = λ(val) { shift(λ(k){ func = k; #    val; #    }); }; ##  `with-yield`      ##   'reset',    ,  ##   'yield' ( )    ##    λ(val) { reset( λ() func(val || yield) ); }; } }; 

, reset . , , , reset . , . , .


:


 with-yield = λ(func) { let (yield) { yield = λ(val) { shift(λ(k){ func = k; val; }); }; λ(val) { reset( λ() func(val || yield) ); }; } }; foo = with-yield(λ(yield){ yield(1); yield(2); yield(3); "DONE"; }); println(foo()); #  1 println(foo()); #  2 println(foo()); #  3 println(foo()); #  DONE 

reset shift


, . . . , , , . Scheme ( — Oleg Kiselyov ). .



, ( CallCC ) . :


 var pstack = []; function _goto(f) { f(function KGOTO(r){ var h = pstack.pop(); h(r); }); } globalEnv.def("reset", function(KRESET, th){ pstack.push(KRESET); _goto(th); }); globalEnv.def("shift", function(KSHIFT, f){ _goto(function(KGOTO){ f(KGOTO, function SK(k1, v){ pstack.push(k1); KSHIFT(v); }); }); }); 

, reset , shift _goto , — . . _goto ( KGOTO ). , f ( CallCC ) - KGOTO , . , f , , KGOTO , , .


reset ( KRESET ) _goto(th) . , , , _goto . , , KGOTO KRESET .


, shift KGOTO , KGOTO pstack , SK , , shift ( shiftKSHIFT ). SK — — ( k1 ) . , shift2 , , pstack.push(k1); :


 println(reset(λ(){ 1 + shift( λ(k) k(k(2)) ); })); println(reset(λ(){ 1 + shift2( λ(k) k(k(2)) ); })); 

Conclusão:


 4 3 ***Result: false 

shift ( k ), reset . — 1 shift :


 1 + [?] 

k , ? . — k(k(2)) . 2 , k(2) 3. , k(3) 3 , 4.


shift2 : k(2) .


CallCC


, , CallCC , . , JS, , , . , CallCC , .


goto , ( ):


 pstack = NIL; goto = false; reset = λ(th) { CallCC(λ(k){ pstack = cons(k, pstack); goto(th); }); }; shift = λ(f) { CallCC(λ(k){ goto(λ(){ f(λ(v){ CallCC(λ(k1){ pstack = cons(k1, pstack); k(v); }); }); }); }); }; let (v = CallCC( λ(k){ goto = k; k(false) } )) { if v then let (r = v(), h = car(pstack)) { pstack = cdr(pstack); h(r); } }; 

— !

Source: https://habr.com/ru/post/pt444024/


All Articles