Scala延续的正式定义

时间:2012-01-13 10:36:44

标签: scala continuations

有一些关于Scala延续的问题(herehere)。但答案只是试图解释它。所以在这个问题中,我要求正确定义什么(Scala的)分隔的延续。我不需要一个例子(虽然它可能会有所帮助),并要求一个简单易懂的形式化,如果它有帮助,甚至可能忽略输入。

形式化应该涵盖语法(不是语法意义上的,而是像f is a function and c is a foo)和语义(计算的结果)。

3 个答案:

答案 0 :(得分:3)

引用wikipedia

  

分隔的延续,可组合的延续或部分延续,是一个“切片”   已经被引入函数的延续框架。

Scala的语法是:

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

上面的延续框架 shift之后执行的所有内容,直到reset分隔的块结束为止。这包括调用函数g,因为它只会在评估shift之后被称为,加上B中的所有代码。

不需要函数g - 可以改为调用方法,或者完全忽略shift的结果。我只是为了表明shift调用返回一个可以使用的值而显示它。

换句话说,该连续帧变为以下功能:

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

整个重置机构变成了这个:

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

请注意,B中的最后一个语句必须包含Y类型。计算结果是shift块内容的结果,就像上面的翻译一样。

如果您想要更精确,check the paper描述Scala中的分隔连续。确切的类型可以在API documentation上找到。

答案 1 :(得分:3)

继续插件中实现的Scala 分隔延续是对 Danvy和引入的 shift reset 控制运算符的改编。 Filinski 。从1990年和1992年看他们的抽象控制代表控制:CPS转型研究 papers。在类型语言的背景下,来自EPFL的工作团队扩展了 Asai 的工作。请参阅2007年的论文here

这应该是充足的形式主义!我瞥了一眼那些,不幸的是他们需要流利的lambda演算符号。

另一方面,我发现了以下使用Shift和Reset进行编程 tutorial,当我开始将示例转换为Scala时,感觉我真的有了突破性的理解。当我得到“2.6如何提取分隔的延续”部分时。

reset运算符分隔程序的一部分。 shift用于存在值的位置(包括可能的单位)。你可以把它想象成一个洞。让我们用represent代表它。

让我们看看以下表达式:

reset { 3 + ◉ - 1 }                  // (1)
reset {                              // (2)
  val s = if (◉) "hello" else "hi"
  s + " world"
}
reset {                              // (3)
  val s = "x" + (◉: Int).toString
  s.length
}

shift做的是将重置内的程序转换为可以访问的函数(此过程称为reification)。在上述情况中,功能是:

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
}

该函数称为continuation,并作为其自身参数的参数提供。转移签名是:

shift[A, B, C](fun: ((A) => B) => C): A 

继续将是(A => B)函数,并且在shift内写入代码的人决定用它做什么(或不做)。如果你简单地归还它,你真的会感受到它能做些什么。然后reset的结果是计算本身:

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
         }

我认为具体化方面确实是理解Scala分隔延续的一个重要方面。

从类型的角度来看,如果函数k具有类型(A => B),则shift具有类型A@cpsParam[B,C]。类型C完全取决于您选择在shift内返回的内容。返回使用cpsParamcps注释的类型的表达式在EPFL论文中被认定为不纯。这与表达式相反,后者没有cps注释类型。

将不纯的计算转换为Shift[A, B, C]个对象(现在在标准库中称为ControlContext[A, B, C])。 Shift个对象正在扩展continuation monad。他们的正式实现在EPFL paper第3.1节第4页中。map方法将纯计算与Shift对象相结合。 flatMap方法将不纯的计算与Shift对象相结合。

continuation插件按照EPLF paper第3.4节中给出的大纲执行代码转换。基本上,shift会变成Shift个对象。之后出现的纯表达式与地图结合,不纯的表达式与flatMaps结合(参见更多规则图4)。最后,一旦所有转换为封闭重置,如果所有类型都进行了类型检查,则所有map和flatMaps之后的最终Shift对象的类型应为Shift[A, A, C]reset函数重新计算包含的Shift并使用identity函数作为参数调用该函数。

总之,我认为EPLF文件确实包含对所发生情况的正式描述(第3.1和3.4节以及图4)。我提到的教程有很好的选择范例,可以很好地理解分隔的延续。

答案 2 :(得分:0)

通过继续操作,您可以在转移后(但在重置内)捕获代码并按以下方式应用代码:

  import scala.util.continuations._
  def main(args: Array[String]): Unit = {
    reset {
      shift { continue: (Int => Int) =>
        val result: Int = continue(continue(continue(7)))
        println("result: " + result) // result: 10
      } + 1
    }
  }

在这种情况下,我们班次之外(但在我们的重置之内)的代码为+1,因此每次您调用继续时,都会应用{_ + 1}。因此continue(continue(continue(7)))的结果是7 +1 +1 +1或10。

这里还有一些来自here的示例代码:

  import scala.util.continuations._
  import java.util.{Timer,TimerTask}

  def main(args: Array[String]): Unit = {
    val timer = new Timer()
    type ContinuationInputType = Unit
    def sleep(delay: Int) = shift { continue: (ContinuationInputType => Unit) =>
      timer.schedule(new TimerTask {
        val nothing: ContinuationInputType = ()
        def run() = continue(nothing) // in a real program, we'd execute our continuation on a thread pool
      }, delay)
    }
    reset {
      println("look, Ma ...")
      sleep(1000)
      println(" no threads!")
    }
  }

在上面的代码中,移位后但在复位内部的代码为println(" no threads!")。因此,如果我们替换它:

def run() = continue(nothing)

与此:

def run() = continue(continue(continue(nothing)))

我们得到以下输出:

look, Ma ...
 no threads!
 no threads!
 no threads!

代替此输出:

look, Ma ...
 no threads!

因此,更改后的代码基本上等于:

  import java.util.{Timer,TimerTask}
  def main(args: Array[String]): Unit = {
    println("look, Ma ...")
    timer.schedule(new TimerTask {
      def run() = {
        println(" no threads!")
        println(" no threads!")
        println(" no threads!")
      }
    }, 1000)
  }

您可以使用代码here

请注意,调用继续之前的所有代码仅执行一次,并且在移位结束和重置结束之间的所有代码执行的次数与调用continue的次数相同。如果从不调用我们的延续,那么在移位结束和重置结束之间的代码将永远不会执行。因此,Scala的延续是一个lambda,它捕获了移位结束和该移位的封闭复位结束之间的所有代码。

还请注意,如果我们的延续是在线程池上执行的,则其余代码(移位结束和重置结束之间的所有代码)将在该线程池提供给我们的线程上执行。因此,如果我们的延续在线程池线程#1上运行,则println(" no threads!")将在线程池线程#1上运行,而println("look, Ma ...")将在主线程上运行。因此,延续功能可用于在异步I / O之上实现外观,使其看起来像阻塞IO。