Pergunta

Acabei de Programação em Scala , e não tenho sido olhando para as alterações entre Scala 2,7 e 2,8. O que parece ser o mais importante é as continuações plugin, mas eu não entendo o que é útil para ou como ele funciona. Eu vi que é bom para Asynchronous I / O, mas eu não tenho sido capaz de descobrir o porquê. Alguns dos recursos mais populares sobre o assunto são os seguintes:

E esta pergunta em Stack Overflow:

Infelizmente, nenhuma destas referências tentar definir o que continuações são ou o que a mudança / funções de reset é suposto fazer, e eu não ter encontrado quaisquer referências que fazem. Eu não tenho sido capaz de adivinhar como qualquer um dos exemplos na artigos trabalho ligado (ou o que eles fazem), então uma maneira de me ajudar poderia ser para ir linha por linha através de uma dessas amostras. Mesmo este simples a partir do terceiro artigo:

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

Por que é o resultado de 8? Isso provavelmente iria me ajudar a começar.

Foi útil?

Solução

blogue não explicar o que reset e shift fazer, então você pode querer ler isso de novo.

Outra fonte boa, que eu também apontam no meu blog, é a entrada da Wikipedia sobre estilo passando continuação . Isso é, de longe, a mais clara sobre o assunto, embora ele não usa sintaxe Scala, e a continuação é passado explicitamente.

O documento sobre continuações delimitados, o que eu apontam para no meu blog, mas parece ter se tornado quebrado, dá muitos exemplos de uso.

Mas eu acho que o melhor exemplo do conceito de continuações delimitados é Scala Swarm. Nele, a biblioteca pára a execução de seu código em um ponto, eo cálculo remanescente torna-se a continuação. A biblioteca, em seguida, faz alguma coisa -. Neste caso, transferindo o cálculo para outro host, e retorna o resultado (o valor da variável que foi acessado) para o cálculo que foi parado

Agora, você não entende mesmo o simples exemplo na página Scala, então não ler o meu blog. Nele eu sou única preocupados em explicar estes princípios, de por que o resultado é 8.

Outras dicas

I found the existing explanations to be less effective at explaining the concept than I would hope. I hope this one is clear (and correct.) I have not used continuations yet.

When a continuation function cf is called:

  1. Execution skips over the rest of the shift block and begins again at the end of it
    • the parameter passed to cf is what the shift block "evaluates" to as execution continues. this can be different for every call to cf
  2. Execution continues until the end of the reset block (or until a call to reset if there is no block)
    • the result of the reset block (or the parameter to reset() if there is no block) is what cf returns
  3. Execution continues after cf until the end of the shift block
  4. Execution skips until the end of the reset block (or a call to reset?)

So in this example, follow the letters from A to 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

This prints:

11
101

Given the canonical example from the research paper for Scala's delimited continuations, modified slightly so the function input to shift is given the name f and thus is no longer anonymous.

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

The Scala plugin transforms this example such that the computation (within the input argument of reset) starting from each shift to the invocation of reset is replaced with the function (e.g. f) input to shift.

The replaced computation is shifted (i.e. moved) into a function k. The function f inputs the function k, where k contains the replaced computation, k inputs x: Int, and the computation in k replaces shift(f) with x.

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

Which has the same effect as:

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

Note the type Int of the input parameter x (i.e. the type signature of k) was given by the type signature of the input parameter of f.

Another borrowed example with the conceptually equivalent abstraction, i.e. read is the function input to 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)
}

I believe this would be translated to the logical equivalent of:

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)
  }
}

I hope this elucidates the coherent common abstraction which was somewhat obfuscated by prior presentation of these two examples. For example, the canonical first example was presented in the research paper as an anonymous function, instead of my named f, thus it was not immediately clear to some readers that it was abstractly analogous to the read in the borrowed second example.

Thus delimited continuations create the illusion of an inversion-of-control from "you call me from outside of reset" to "I call you inside reset".

Note the return type of f is, but k is not, required to be the same as the return type of reset, i.e. f has the freedom to declare any return type for k as long as f returns the same type as reset. Ditto for read and capture (see also ENV below).


Delimited continuations do not implicitly invert the control of state, e.g. read and callback are not pure functions. Thus the caller can not create referentially transparent expressions and thus does not have declarative (a.k.a. transparent) control over intended imperative semantics.

We can explicitly achieve pure functions with delimited continuations.

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)
  }
}

I believe this would be translated to the logical equivalent of:

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)
    }
  }
}

This is getting noisy, because of the explicit environment.

Tangentially note, Scala does not have Haskell's global type inference and thus as far as I know couldn't support implicit lifting to a state monad's unit (as one possible strategy for hiding the explicit environment), because Haskell's global (Hindley-Milner) type inference depends on not supporting diamond multiple virtual inheritance.

Continuation capture the state of a computation, to be invoked later.

Think of the computation between leaving the shift expression and leaving the reset expression as a function. Inside the shift expression this function is called k, it is the continuation. You can pass it around, invoke it later, even more than once.

I think the value returned by the reset expression is the value of the expression inside the shift expression after the =>, but about this I'm not quite sure.

So with continuations you can wrap up a rather arbitrary and non-local piece of code in a function. This can be used to implement non-standard control flow, such as coroutining or backtracking.

So continuations should be used on a system level. Sprinkling them through your application code would be a sure recipe for nightmares, much worse than the worst spaghetti code using goto could ever be.

Disclaimer: I have no in depth understanding of continuations in Scala, I just inferred it from looking at the examples and knowing continuations from Scheme.

From my point of view, the best explanation was given here: http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html

One of examples:

To see the control flow a little more clearly, you can execute this code snippet:

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")
}

Here's the output the above code produces:

A
B
D
E
G
F
C

Another (more recent -- May 2016) article on Scala continuations is:
"Time Travel in Scala: CPS in Scala (scala’s continuation)" by Shivansh Srivastava (shiv4nsh).
It also refers to Jim McBeath's article mentioned in Dmitry Bespalov's answer.

But before that, it describes Continuations like so:

A continuation is an abstract representation of the control state of a computer program.
So what it actually means is that it is a data structure that represents the computational process at a given point in the process’s execution; the created data structure can be accessed by the programming language, instead of being hidden in the runtime environment.

To explain it further we can have one of the most classic example,

Say you’re in the kitchen in front of the refrigerator, thinking about a sandwich. You take a continuation right there and stick it in your pocket.
Then you get some turkey and bread out of the refrigerator and make yourself a sandwich, which is now sitting on the counter.
You invoke the continuation in your pocket, and you find yourself standing in front of the refrigerator again, thinking about a sandwich. But fortunately, there’s a sandwich on the counter, and all the materials used to make it are gone. So you eat it. :-)

In this description, the sandwich is part of the program data (e.g., an object on the heap), and rather than calling a “make sandwich” routine and then returning, the person called a “make sandwich with current continuation” routine, which creates the sandwich and then continues where execution left off.

That being said, as announced in April 2014 for Scala 2.11.0-RC1

We are looking for maintainers to take over the following modules: scala-swing, scala-continuations.
2.12 will not include them if no new maintainer is found.
We will likely keep maintaining the other modules (scala-xml, scala-parser-combinators), but help is still greatly appreciated.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top