Continuações de Scala por meio de exemplos significativos
Vamos definir o from0to10
que expressa a ideia de iteração de 0 a 10:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Agora,
reset {
val x = from0to10()
print(s"$x ")
}
println()
estampas:
0 1 2 3 4 5 6 7 8 9 10
Na verdade, não precisamos x
:
reset {
print(s"${from0to10()} ")
}
println()
imprime o mesmo resultado.
E
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
imprime todos os pares:
(0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7) (0,8) (0,9) (0,10) (1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7) (1,8) (1,9) (1,10) (2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7) (2,8) (2,9) (2,10) (3,0) (3,1) (3,2) (3,3) (3,4) (3,5) (3,6) (3,7) (3,8) (3,9) (3,10) (4,0) (4,1) (4,2) (4,3) (4,4) (4,5) (4,6) (4,7) (4,8) (4,9) (4,10) (5,0) (5,1) (5,2) (5,3) (5,4) (5,5) (5,6) (5,7) (5,8) (5,9) (5,10) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (6,7) (6,8) (6,9) (6,10) (7,0) (7,1) (7,2) (7,3) (7,4) (7,5) (7,6) (7,7) (7,8) (7,9) (7,10) (8,0) (8,1) (8,2) (8,3) (8,4) (8,5) (8,6) (8,7) (8,8) (8,9) (8,10) (9,0) (9,1) (9,2) (9,3) (9,4) (9,5) (9,6) (9,7) (9,8) (9,9) (9,10) (10,0) (10,1) (10,2) (10,3) (10,4) (10,5) (10,6) (10,7) (10,8) (10,9) (10,10)
Agora, como isso funciona?
Há o código de chamada , from0to10
e o código de chamada . Nesse caso, é o bloco que se segue reset
. Um dos parâmetros passados para o código chamado é um endereço de retorno que mostra qual parte do código de chamada ainda não foi executada (**). Essa parte do código de chamada é a continuação . O código chamado pode fazer com aquele parâmetro tudo o que decidir: passar o controle para ele, ou ignorar, ou chamá-lo várias vezes. Aqui from0to10
chama essa continuação para cada inteiro no intervalo de 0 a 10.
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Mas onde termina a continuação? Isso é importante porque o último return
da continuação retorna o controle para o código chamado from0to10
,. No Scala, termina onde o reset
bloco termina (*).
Agora, vemos que a continuação é declarada como cont: Int => Unit
. Por quê? Chamamos from0to10
as val x = from0to10()
e Int
é o tipo de valor que vai para x
. Unit
significa que o bloco posterior não reset
deve retornar nenhum valor (caso contrário, haverá um erro de tipo). Em geral, existem 4 tipos de assinaturas: entrada de função, entrada de continuação, resultado de continuação, resultado de função. Todos os quatro devem corresponder ao contexto de invocação.
Acima, imprimimos pares de valores. Vamos imprimir a tabuada de multiplicação. Mas como fazemos a saída \n
após cada linha?
A função back
nos permite especificar o que deve ser feito quando o controle retorna, desde a continuação até o código que o chamou.
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
back
primeiro chama sua continuação e, em seguida, executa a ação .
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ")
}
Ele imprime:
0 0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9 10
0 2 4 6 8 10 12 14 16 18 20
0 3 6 9 12 15 18 21 24 27 30
0 4 8 12 16 20 24 28 32 36 40
0 5 10 15 20 25 30 35 40 45 50
0 6 12 18 24 30 36 42 48 54 60
0 7 14 21 28 35 42 49 56 63 70
0 8 16 24 32 40 48 56 64 72 80
0 9 18 27 36 45 54 63 72 81 90
0 10 20 30 40 50 60 70 80 90 100
Bem, agora é hora de alguns quebra-cabeças. Existem duas invocações de from0to10
. Qual é a continuação do primeiro from0to10
? Ele segue a invocação de from0to10
no código binário , mas no código-fonte também inclui a instrução de atribuição val i =
. Ele termina onde o reset
bloco termina, mas o final do reset
bloco não retorna o controle para o primeiro from0to10
. O final do reset
bloco retorna o controle para o segundo from0to10
, que por sua vez eventualmente retorna o controle para back
, e é ele back
que retorna o controle para a primeira invocação de from0to10
. Quando o primeiro (sim! 1º!) from0to10
Sai, todo o reset
bloco é encerrado.
Esse método de retorno de controle é chamado de backtracking , é uma técnica muito antiga, conhecida pelo menos desde os tempos de Prolog e derivados Lisp orientados para IA.
Os nomes reset
e shift
são errôneos. Esses nomes deveriam ter sido deixados para as operações bit a bit. reset
define limites de continuação e shift
obtém uma continuação da pilha de chamadas.
Notas)
(*) Em Scala, a continuação termina onde o reset
bloco termina. Outra abordagem possível seria deixar terminar onde termina a função.
(**) Um dos parâmetros do código chamado é um endereço de retorno que mostra qual parte do código de chamada ainda não foi executada. Bem, no Scala, uma sequência de endereços de retorno é usada para isso. Quantos? Todos os endereços de retorno colocados na pilha de chamadas desde a entrada no reset
bloco.
UPD Parte 2
Descartando Continuações: Filtragem
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
Isso imprime:
0 2 4 6 8 10
Vamos fatorar duas operações importantes: descartar a continuação ( fail()
) e passar o controle para ela ( succ()
):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
Ambas as versões de succ()
(acima) funcionam. Acontece que shift
tem uma assinatura engraçada e, embora succ()
não faça nada, deve ter essa assinatura para o equilíbrio do tipo.
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
como esperado, ele imprime
0 2 4 6 8 10
Dentro de uma função, succ()
não é necessário:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
novamente, imprime
0 2 4 6 8 10
Agora, vamos definir onOdd()
via onEven()
:
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
try {
reset {
onEven(x)
throw new ControlTransferException()
}
cont()
} catch {
case e: ControlTransferException =>
case t: Throwable => throw t
}
}
reset {
back { println() }
val x = from0to10()
onOdd(x)
print(s"$x ")
}
Acima, se x
for par, uma exceção é lançada e a continuação não é chamada; se x
for ímpar, a exceção não é lançada e a continuação é chamada. O código acima é impresso:
1 3 5 7 9