Formal Definition of Scala Continuations

There are some questions about what Scala continues ( here and here ). But the answers are only trying to explain this. Therefore, in this matter I ask for a formal definition of what (Scala) is limited to. I do not need an example (although it can help) and ask for a formalization as simple and understandable as possible, perhaps even ignoring input if that helps.

Formalization should encompass syntax (not in a grammatical sense, but rather as f is a function and c is a foo ) and semantics (which will be the result of the calculation).

+7
source share
2 answers

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.

+3
source

To quote wikipedia :

a limited continuation, a composite continuation or a partial continuation, is a β€œslice" continuation frame that was included in the function.

Scala syntax for this:

 // Assuming g: X => anything reset { A g(shift { (f: (X) => Y) => /* code using function f */ }) B } 

The next continuation frame is all that would be done after shift to the end of the block, separated by the reset symbol. This involves calling g , because it is only called after evaluating shift , as well as all the code in B

The g function is not required - you could call the method instead or completely ignore the result of shift . I am just showing that calling shift returns a value that can be used.

In other words, this continuation frame becomes the following function:

 // Assuming g: X => anything def f: (X) => Y = { x => g(x) B } 

And the whole body reset will become as follows:

 // Assuming g: X => anything A def f: (X) => Y = { x => g(x) B } /* code using function f */ 

Note that the last statement in B must be of type Y The result of the calculation is the result of the contents of the shift block, as would be the case with the above translation.

If you need better accuracy, check out paper that describes Scala's delimited sequels. Exact types can be found in the API documentation .

+3
source

All Articles