Scala limited continuations implemented in the continuation plugin are an adaptation of the shift and reset control operators introduced by Danvi and Filinski. See Their control over abstraction and presentation of management: a study of the transformation of CPS papers in 1990 and 1992. In the context of a typed language, work from the EPFL team extends Asai's work. See Articles 2007 here .
This should be a lot of formalism! I looked at them, and, unfortunately, they need fluency in the notation of lambda calculus.
On the other hand, I found the following Programming with Shift and reset tutorial , and it seems to me that I really had a breakthrough in understanding when I started translating examples into Scala and when I got to the section "2.6. How to extract delimited sequels".
The reset
statement limits a portion of a program. shift
used in the place where the value is present (including, possibly, Unit). You can think of it as a hole. Imagine it through β.
So, look at the following expressions:
reset { 3 + β - 1 } // (1) reset { // (2) val s = if (β) "hello" else "hi" s + " world" } reset { // (3) val s = "x" + (β: Int).toString s.length }
What shift
is to turn the program inside reset into a function that you can access (this process is called reification). In the above cases, the functions:
val f1 = (i: Int) => 3 + i - 1 // (1) val f2 = (b: Boolean) => { val s = if (b) "hello" else "hi" // (2) s + " world" } val f3 = (i: Int) => { // (3) val s = "x" + i.toString s.length }
The function is called a continuation and is provided as an argument to its own argument. shift signature:
shift[A, B, C](fun: ((A) => B) => C): A
The continuation will be a function (A => B), and the one who writes the code inside shift
decides what to do (or not to do) with it. You really feel what he can do if you just return it. The result of reset
is that the initialized calculation itself:
val f1 = reset { 3 + shift{ (k:Int=>Int) => k } - 1 } val f2 = reset { val s = if (shift{(k:Boolean=>String) => k}) "hello" else "hi" s + " world" } val f3 = reset { val s = "x" + (shift{ (k:Int=>Int) => k}).toString s.length }
I think the reification aspect is a really important aspect of understanding Scala to continue.
From the point of view of type, if the function k
is of type (A => B), then shift
is of type A@cpsParam [B,C]
. Type C
determined only by what you have chosen to return inside shift
. An expression returning a type annotated with cpsParam
or cps
qualifies as unclean in the EPFL document. This is in contrast to the pure expression, which does not have cps-annotated types.
Impure calculations are converted to Shift[A, B, C]
objects (now called ControlContext[A, B, C]
in the standard library). Shift objects extend the continuation monad. Their formal implementation is given in the EPFL document, section 3.1, page 4. The map
method combines pure computation with a shift
object. The flatMap
method combines an impure calculation with a shift
object.
The continuation plugin performs code conversion following the scheme in section 3.4 of the EPLF document. Basically, shift
turns into shift
objects. Pure expressions that occur after combining with cards are combined with unclean expressions with flatMaps (see More rules, Figure 4). Finally, once everything has been converted to an enveloping reset, if all types of checks, the type of the final Shift object after all maps and flat maps should be Shift[A, A, C]
. The reset
function updates the contained shift
and calls the function with the identification function as an argument.
In conclusion, I believe that the EPLF contains a formal description of what is happening (sections 3.1 and 3.4 and figure 4). The mention I mention has very well-chosen examples that look great at delimited sequels.