Estilo de passagem de continuação - Continuation-passing style
Na programação funcional , o estilo de passagem de continuação ( CPS ) é um estilo de programação no qual o controle é passado explicitamente na forma de uma continuação . Isso é contrastado com o estilo direto , que é o estilo usual de programação. Gerald Jay Sussman e Guy L. Steele, Jr. cunharam a frase em AI Memo 349 (1975), que apresenta a primeira versão da linguagem de programação Scheme . John C. Reynolds dá um relato detalhado das numerosas descobertas de continuações.
Uma função escrita no estilo de passagem de continuação leva um argumento extra: uma "continuação" explícita; ou seja , uma função de um argumento. Quando a função CPS calcula seu valor de resultado, ela o "retorna" chamando a função de continuação com esse valor como argumento. Isso significa que, ao invocar uma função CPS, a função de chamada deve fornecer um procedimento a ser invocado com o valor de "retorno" da sub-rotina. Expressar código nesta forma torna várias coisas explícitas que estão implícitas no estilo direto. Isso inclui: retornos de procedimento, que se tornam aparentes como chamadas para uma continuação; valores intermediários, que são todos nomes dados; ordem de avaliação do argumento, que é explicitada; e chamadas finais , que simplesmente chamam um procedimento com a mesma continuação, sem modificações, que foi passado para o chamador.
Os programas podem ser transformados automaticamente de estilo direto em CPS. Compiladores funcionais e lógicos geralmente usam o CPS como uma representação intermediária onde um compilador para uma linguagem de programação imperativa ou procedural usaria o formulário de atribuição única estática (SSA). SSA é formalmente equivalente a um subconjunto do CPS (excluindo o fluxo de controle não local, que não ocorre quando o CPS é usado como representação intermediária). Compiladores funcionais também podem usar a forma normal A (ANF) (mas apenas para linguagens que requerem avaliação rápida), em vez de ' thunks ' (descrito nos exemplos abaixo) no CPS. O CPS é usado com mais frequência por compiladores do que por programadores como um estilo local ou global.
Exemplos
No CPS, cada procedimento leva um argumento extra que representa o que deve ser feito com o resultado que a função está calculando. Isso, junto com um estilo restritivo que proíbe uma variedade de construções normalmente disponíveis, é usado para expor a semântica dos programas, tornando-os mais fáceis de analisar. Esse estilo também facilita a expressão de estruturas de controle incomuns, como pegar / lançar ou outras transferências de controle não locais.
A chave para o CPS é lembrar que (a) cada função leva um argumento extra conhecido como continuação e (b) cada argumento em uma chamada de função deve ser uma variável ou uma expressão lambda (não uma expressão mais complexa). Isso tem o efeito de virar as expressões "de dentro para fora" porque as partes mais internas da expressão devem ser avaliadas primeiro, portanto, o CPS torna explícita a ordem de avaliação, bem como o fluxo de controle. Alguns exemplos de código no estilo direto e o CPS correspondente aparecem abaixo. Esses exemplos são escritos na linguagem de programação Scheme ; por convenção, a função de continuação é representada como um parâmetro denominado " k
":
Estilo direto |
Estilo de passe de continuação
|
---|---|
(define (pyth x y)
(sqrt (+ (* x x) (* y y))))
|
(define (pyth& x y k)
(*& x x (lambda (x2)
(*& y y (lambda (y2)
(+& x2 y2 (lambda (x2py2)
(sqrt& x2py2 k))))))))
|
(define (factorial n)
(if (= n 0)
1 ; NOT tail-recursive
(* n (factorial (- n 1)))))
|
(define (factorial& n k)
(=& n 0 (lambda (b)
(if b ; growing continuation
(k 1) ; in the recursive call
(-& n 1 (lambda (nm1)
(factorial& nm1 (lambda (f)
(*& n f k)))))))))
|
(define (factorial n)
(f-aux n 1))
(define (f-aux n a)
(if (= n 0)
a ; tail-recursive
(f-aux (- n 1) (* n a))))
|
(define (factorial& n k) (f-aux& n 1 k))
(define (f-aux& n a k)
(=& n 0 (lambda (b)
(if b ; unmodified continuation
(k a) ; in the recursive call
(-& n 1 (lambda (nm1)
(*& n a (lambda (nta)
(f-aux& nm1 nta k)))))))))
|
Observe que nas versões CPS, os primitivos usados, como +&
e *&
são eles próprios CPS, não estilo direto, portanto, para fazer os exemplos acima funcionarem em um sistema Scheme, precisaríamos escrever essas versões CPS dos primitivos, com, por exemplo, *&
definido por:
(define (*& x y k)
(k (* x y)))
Para fazer isso em geral, podemos escrever uma rotina de conversão:
(define (cps-prim f)
(lambda args
(let ((r (reverse args)))
((car r) (apply f
(reverse (cdr r)))))))
(define *& (cps-prim *))
(define +& (cps-prim +))
Para chamar um procedimento escrito no CPS a partir de um procedimento escrito no estilo direto, é necessário fornecer uma continuação que receberá o resultado calculado pelo procedimento CPS. No exemplo acima (assumindo que primitivas de estilo CPS foram fornecidas), podemos chamar (factorial& 10 (lambda (x) (display x) (newline)))
.
Existe alguma variedade entre os compiladores na maneira como as funções primitivas são fornecidas no CPS. Acima, usamos a convenção mais simples; no entanto, às vezes são fornecidos primitivos booleanos que levam dois thunks para serem chamados nos dois casos possíveis, de modo que a (=& n 0 (lambda (b) (if b ...)))
chamada dentro da f-aux&
definição acima seria escrita em vez de (=& n 0 (lambda () (k a)) (lambda () (-& n 1 ...)))
. Da mesma forma, às vezes a if
própria primitiva não é incluída no CPS e, em vez disso, if&
é fornecida uma função que leva três argumentos: uma condição booleana e os dois thunks correspondentes aos dois braços da condicional.
As traduções mostradas acima mostram que o CPS é uma transformação global. O fatorial de estilo direto leva, como era de se esperar, um único argumento; o fatorial CPS e leva dois: o argumento e uma continuação. Qualquer função que chame uma função ed CPS deve fornecer uma nova continuação ou passar a sua própria; quaisquer chamadas de uma função ed CPS para uma função não CPS usarão continuações implícitas. Assim, para garantir a ausência total de uma pilha de funções, todo o programa deve estar no CPS.
CPS em Haskell
Nesta seção, escreveremos uma função pyth
que calcula a hipotenusa usando o teorema de Pitágoras . Uma implementação tradicional da pyth
função se parece com isto:
pow2 :: Float -> Float
pow2 a = a ** 2
add :: Float -> Float -> Float
add a b = a + b
pyth :: Float -> Float -> Float
pyth a b = sqrt (add (pow2 a) (pow2 b))
Para transformar a função tradicional em CPS, precisamos mudar sua assinatura. A função obterá outro argumento do tipo de função, e seu tipo de retorno depende dessa função:
pow2' :: Float -> (Float -> a) -> a
pow2' a cont = cont (a ** 2)
add' :: Float -> Float -> (Float -> a) -> a
add' a b cont = cont (a + b)
-- Types a -> (b -> c) and a -> b -> c are equivalent, so CPS function
-- may be viewed as a higher order function
sqrt' :: Float -> ((Float -> a) -> a)
sqrt' a = \cont -> cont (sqrt a)
pyth' :: Float -> Float -> (Float -> a) -> a
pyth' a b cont = pow2' a (\a2 -> pow2' b (\b2 -> add' a2 b2 (\anb -> sqrt' anb cont)))
Primeiro calculamos o quadrado de a em pyth'
função e passamos uma função lambda como uma continuação que aceitará um quadrado de a como primeiro argumento. E assim por diante até chegarmos ao resultado de nossos cálculos. Para obter o resultado desta função podemos passar id
função como um argumento final que retorna o valor que foi passado para ele inalterada: pyth' 3 4 id == 5.0
.
A biblioteca mtl, que é enviada com o GHC , tem o módulo Control.Monad.Cont
. Este módulo fornece o tipo Cont, que implementa Monad e algumas outras funções úteis. O snippet a seguir mostra a pyth'
função usando Cont:
pow2_m :: Float -> Cont a Float
pow2_m a = return (a ** 2)
pyth_m :: Float -> Float -> Cont a Float
pyth_m a b = do
a2 <- pow2_m a
b2 <- pow2_m b
anb <- cont (add' a2 b2)
r <- cont (sqrt' anb)
return r
Não apenas a sintaxe se tornou mais limpa, mas esse tipo nos permite usar uma função callCC
com tipo MonadCont m => ((a -> m b) -> m a) -> m a
. Esta função possui um argumento de um tipo de função; esse argumento de função também aceita a função, o que descarta todos os cálculos posteriores à sua chamada. Por exemplo, vamos interromper a execução da pyth
função se pelo menos um de seus argumentos for negativo, retornando zero:
pyth_m :: Float -> Float -> Cont a Float
pyth_m a b = callCC $ \exitF -> do -- $ sign helps to avoid parentheses: a $ b + c == a (b + c)
when (b < 0 || a < 0) (exitF 0.0) -- when :: Applicative f => Bool -> f () -> f ()
a2 <- pow2_m a
b2 <- pow2_m b
anb <- cont (add' a2 b2)
r <- cont (sqrt' anb)
return r
Continuações como objetos
A programação com continuações também pode ser útil quando um chamador não deseja esperar até que o receptor seja concluído. Por exemplo, na programação da interface do usuário (IU), uma rotina pode configurar campos de caixa de diálogo e passá-los, junto com uma função de continuação, para a estrutura da IU. Essa chamada retorna imediatamente, permitindo que o código do aplicativo continue enquanto o usuário interage com a caixa de diálogo. Assim que o usuário pressiona o botão "OK", o framework chama a função de continuação com os campos atualizados. Embora este estilo de codificação use continuações, não é CPS completo.
function confirmName() {
fields.name = name;
framework.Show_dialog_box(fields, confirmNameContinuation);
}
function confirmNameContinuation(fields) {
name = fields.name;
}
Uma ideia semelhante pode ser usada quando a função deve ser executada em um thread diferente ou em um processador diferente. A estrutura pode executar a função chamada em um thread de trabalho e, em seguida, chamar a função de continuação no thread original com os resultados do trabalhador. Isso está em Java usando a estrutura de IU Swing :
void buttonHandler() {
// This is executing in the Swing UI thread.
// We can access UI widgets here to get query parameters.
final int parameter = getField();
new Thread(new Runnable() {
public void run() {
// This code runs in a separate thread.
// We can do things like access a database or a
// blocking resource like the network to get data.
final int result = lookup(parameter);
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
// This code runs in the UI thread and can use
// the fetched data to fill in UI widgets.
setField(result);
}
});
}
}).start();
}
Usar a notação lambda Java 8 acima encurta o código para (a final
palavra-chave pode ser ignorada):
void buttonHandler() {
int parameter = getField();
new Thread(() -> {
int result = lookup(parameter);
javax.swing.SwingUtilities.invokeLater(() -> setField(result));
}).start();
}
Chamadas de cauda
Cada chamada no CPS é uma chamada final e a continuação é explicitamente passada. Usar o CPS sem otimização de chamada final (TCO) fará com que não apenas a continuação construída aumente potencialmente durante a recursão, mas também a pilha de chamadas . Isso geralmente é indesejável, mas tem sido usado de maneiras interessantes - consulte o compilador Chicken Scheme . Como o CPS e o TCO eliminam o conceito de um retorno de função implícito, seu uso combinado pode eliminar a necessidade de uma pilha de tempo de execução. Vários compiladores e interpretadores para linguagens de programação funcionais usam essa capacidade de novas maneiras.
Uso e implementação
O estilo de aprovação de continuação pode ser usado para implementar continuações e operadores de fluxo de controle em uma linguagem funcional que não apresenta continuações de primeira classe, mas tem funções de primeira classe e otimização de chamada final . Sem a otimização de chamada de cauda, técnicas como trampolim , ou seja, usando um loop que invoca iterativamente funções de retorno de conversão , podem ser usadas; sem funções de primeira classe, é até possível converter chamadas finais em apenas gotos em tal loop.
Escrever código em CPS, embora não seja impossível, costuma estar sujeito a erros. Existem várias traduções, geralmente definidas como conversões de uma ou duas passagens de cálculo lambda puro , que convertem expressões de estilo direto em expressões CPS. Escrever no estilo trampolim, no entanto, é extremamente difícil; quando usado, geralmente é o alvo de algum tipo de transformação, como compilação .
Funções que usam mais de uma continuação podem ser definidas para capturar vários paradigmas de fluxo de controle, por exemplo (no Esquema ):
(define (/& x y ok err)
(=& y 0.0 (lambda (b)
(if b
(err (list "div by zero!" x y))
(ok (/ x y))))))
É importante notar que a transformação CPS é conceitualmente uma incorporação de Yoneda . Também é semelhante à incorporação do cálculo lambda no cálculo π .
Use em outros campos
Fora da ciência da computação , o CPS é de interesse mais geral como uma alternativa ao método convencional de composição de expressões simples em expressões complexas. Por exemplo, dentro da semântica linguística , Chris Barker e seus colaboradores sugeriram que a especificação das denotações de sentenças usando o CPS pode explicar certos fenômenos na linguagem natural .
Em matemática , o isomorfismo de Curry-Howard entre programas de computador e provas matemáticas relaciona a tradução do estilo de passagem de continuação a uma variação das incorporações de dupla negação da lógica clássica na lógica intuicionista (construtiva) . Ao contrário da tradução de dupla negação regular , que mapeia as proposições atômicas p para (( p → ⊥) → ⊥), o estilo de passagem de continuação substitui ⊥ pelo tipo da expressão final. Consequentemente, o resultado é obtido passando a função de identidade como uma continuação para a expressão do estilo CPS, como no exemplo acima.
A própria lógica clássica está relacionada à manipulação da continuação de programas diretamente, como no operador de controle de continuação de chamada com corrente de Scheme , uma observação devida a Tim Griffin (usando o operador de controle C intimamente relacionado).
Veja também
Notas
Referências
- Continuation Passing C (CPC) - linguagem de programação para escrever sistemas concorrentes , desenhada e desenvolvida por Juliusz Chroboczek e Gabriel Kerneis. repositório github
- A construção de um compilador baseado em CPS para ML é descrita em: Appel, Andrew W. (1992). Compilando com continuações . Cambridge University Press. ISBN 978-0-521-41695-5.
- Danvy, Olivier ; Filinski, Andrzej (1992). "Representando o controle, um estudo da transformação do CPS". Estruturas matemáticas em ciência da computação . 2 (4): 361–391. CiteSeerX 10.1.1.46.84 . doi : 10.1017 / S0960129500001535 .
- Compilador Chicken Scheme , um compilador Scheme para C que usa o estilo de passagem de continuação para traduzir procedimentos Scheme em funções C enquanto usa a pilha C como berçário para o coletor de lixo geracional
- Kelsey, Richard A. (março de 1995). "Uma correspondência entre o estilo de aprovação de continuação e o formulário de atribuição única estática". Avisos ACM SIGPLAN . 30 (3): 13–22. CiteSeerX 10.1.1.3.6773 . doi : 10.1145 / 202530.202532 .
- Appel, Andrew W. (abril de 1998). "SSA é programação funcional" . Avisos ACM SIGPLAN . 33 (4): 17–20. CiteSeerX 10.1.1.34.3282 . doi : 10.1145 / 278283.278285 .
- Danvy, Olivier; Millikin, Kevin; Nielsen, Lasse R. (2007). "Em transformações CPS de uma passagem" . BRICS, Departamento de Ciência da Computação, Universidade de Aarhus. p. 24. ISSN 0909-0878 . RS-07-6 . Página visitada em 26 de outubro de 2007 .
- Dybvig, R. Kent (2003). A linguagem de programação do esquema . Prentice Hall. p. 64Link direto: "Seção 3.4. Estilo de aprovação de continuação" .