1. 程式人生 > >快學Scala學習筆記及習題解答(21-22隱式轉換和隱式引數、定界延續)

快學Scala學習筆記及習題解答(21-22隱式轉換和隱式引數、定界延續)

本文Scala使用的版本是2.11.8

第21章 隱式轉換和隱式引數

21.1 基本概念

所謂隱式轉換函式(implicit conversion function)指的是那種以implicit關鍵字宣告的帶有單個引數的函式。這樣的函式將被自動應用,將值從一種型別轉換為另一種型別。

class Fraction() {

    private var n: Int = 0
    private var m: Int = 0

    def this(n: Int, m: Int) {
        this
        this.n = n
        this
.m = m } def *(that: Fraction): Fraction = { Fraction(this.n * that.n, this.m * that.m) } override def toString() = { this.n + " " + this.m } } object Fraction { def apply(n: Int, m: Int) = { new Fraction(n, m) } } object C21_1 { // 定義隱式轉換函式
implicit def int2Fraction(n: Int) = Fraction(n, 1) def main(args: Array[String]) { // // 將呼叫int2Fraction,將整數3轉換成一個Fraction物件。 val result = 3 * Fraction(4, 5) println(result) } }

21.2 利用隱式轉換豐富現有類庫的功能

val contents = new File("README").read

提供上面功能,需要:

class RichFile(val from
: File) { def read = Source.fromFile(from.getPath).mkString } // 再提供一個隱式轉換 implicit def file2RichFile(from: File) = new RichFile(from)

21.3 引入隱式轉換

Scala會考慮如下的隱式轉換函式:

  1. 位於源或目標型別伴生物件中的隱式函式。
  2. 位於當前作用域可以以單個識別符號指代的隱式函式。

比如把int2Fraction函式放入FractionConversions物件中,而這個物件位於com.zw.impatient包,如果想引入,就像這樣:

import com.zw.impatient.FractionConversions._

也可以區域性引入,或將某個特定隱式轉換排除(見第7章)。

21.4 隱式轉換規則

隱式轉換在如下三種各不相同的情況會被考慮:

  • 當表示式的型別與預期的型別不同時
  • 當物件訪問一個不存在的成員時
  • 當物件呼叫某個方法,而該方法的引數宣告與傳入引數不匹配時

另一方面,有三種情況編譯器不會嘗試使用隱式轉換:

  • 如果程式碼能夠在不使用隱式轉換的前提下通過編譯,則不會使用隱式轉換。
  • 編譯器不會嘗試同時執行多個轉換,比如convert1(convert2(a))*b
  • 存在二義性的轉換是個錯誤。例如,如果convert1(a)*bconvert2(a)*b都是合法的,編譯器將會報錯。


通過以下編譯選項,可以檢視編譯器使用了哪些隱式轉換。

scalac -Xprint:typer MyProg.scala

21.5 隱式引數

函式或方法可以帶有一個標記為implicit的引數列表。這種情況下,編譯器將會查詢預設值,提供給該函式或方法。

// 以下兩種引入都可以
//import com.zw.scala.chapter.twentyone.FrenchPunctuation._
import com.zw.scala.chapter.twentyone.FrenchPunctuation.quoteDelimiters

/**
  * Created by zhangws on 17/2/14.
  */
case class Delimiters(left: String, right: String)

object FrenchPunctuation {
    implicit val quoteDelimiters = Delimiters("<<", ">>")
}

object C21_5 {

    def quote(what: String)(implicit delims: Delimiters) =
        println(delims.left + what + delims.right)

    def main(args: Array[String]) {

        quote("Bonjour le monde")(Delimiters("<", ">"))

        // 這種情況下,編譯器將會查詢一個型別為Delimiters的隱式值。這必須是一個被宣告為implicit的值
        quote("Bonjour le monde")
    }
}

編譯器將會在如下兩個地方查詢這樣的一個物件:

  • 在當前作用域所有可以用單個識別符號指代的滿足型別要求的val和def。
  • 與所要求型別相關聯的型別的伴生物件。相關聯的型別包括所要求型別本身,以及它的型別引數(如果它是一個引數化的型別的話)。

21.6 利用隱式引數進行隱式轉換

隱式的函式引數也可以被用做隱式轉換。

// 泛型函式
def smaller[T](a: T, b: T) = if (a < b) a else b

// 應該寫成下面形式,否則編譯器不會接受這個函式
def smaller[T](a: T, b: T)(implicit order: T => Ordered[T])
    = if (a < b) a else b

注意order是一個帶有單個引數的函式,被打上了implicit標籤,並且有一個以單個識別符號出現的名稱。因此,它不僅是一個隱式引數,它還是一個隱式轉換。

21.7 上下文界定

型別引數可以有一個形式為T: M的上下文界定,其中M是另一個泛型型別。它要求作用域中存在一個型別為M[T]的隱式值。例如:

class Pair[T: Ordering]

要求存在一個型別為Ordering[T]的隱式值。該隱式值可以被用在該類的方法當中:

class Pair[T: Ordering](val first: T, val second: T) {
    def smaller(implicit ord: Ordering[T]) =
        if (ord.compare(first, second) < 0) first else second
}

如果new Pair(40, 2),編譯器將推斷出我們需要一個Pair[Int]。由於Predef作用域中有一個型別為Ordering[Int]的隱式值,因此Int滿足上下文界定。這個Ordering[Int]就成為該類的一個欄位,被傳入需要該值得方法當中。

如果願意,也可以用Predef類的implicitly方法獲取該值:

class Pair[T: Ordering](val first: T, val second: T) {
    def smaller = if (implicitly[Ordering[T]].compare(first, second) < 0) first else second
}

implicitly函式在Predef.scala中定義如下:

def implicitly[T](implicit e: T) = e
// 用於從冥界召喚隱式值

或者,也可以利用Ordered特質中定義的從Ordering到Ordered的隱式轉換。一旦引入了這個轉換,就可以使用關係操作符:

class Pair[T: Ordering](val first: T, val second: T) {
    def smaller = {
        import Ordered._;
        if (first < second) first else second
    }
}

重要的是可以隨時例項化Pair[T],只要滿足存在型別為Ordering[T]的隱式值的條件即可。例如,想要Pair[Point],則可以組織一個隱式的Ordering[Point]值:

implicit object PointOrdering extends Ordering[Point] {
    def compare(a: Point, b: Point) = ...
}

21.8 型別證明

def firstLast[A, C](it: C)(implicit ev: C <:< Iterable[A]) = 
    (it.head, it.last)

=:=、<:<和<%<是帶有隱式值的類,定義在Predef物件當中。例如,<:<從本質上講就是:

abstract class <:<[-From, +To] extends Function1[From, To]

object <:< {
    implicit def conforms[A] = new (A <:< A) { def apply(x: A) = x }
}

假定編輯器需要處理約束implicit ev: String <:< AnyRef。它會在伴生物件中查詢型別為String <:< AnyRef的隱式物件。因此如下物件:

<:<.conforms[String]

可以被當做String <:< AnyRef的例項使用。

我們把ev稱做 “型別證明物件” ——它的存在證明了如下事實:以本例來說,String是AnyRef的子型別。

這裡的型別證明物件是恆等函式(即永遠返回引數原值的函式)。恆等函式是必需的原因如下:


def firstLast[A, C](it: C)(implicit ev: C 

21.9 @implicitNotFound註解

@implicitNotFound註解告訴編譯器在不能構造出帶有該註解的型別的引數時給出錯誤提示。例如:

@implicitNotFound(msg = "Cannot prove that ${From} <:< ${To}."
abstract class <:<[-From, +To] extends Function1[From, To]

// 如下呼叫
firstLast[String, List[Int]](List(1, 2, 3))

// 則錯誤提示為
Cannot prove that List[Int] <:< Iterable[String]

其中${From}${To}將被替換成被註解類的型別引數From和To。

21.10 CanBuildFrom解讀

map是一個Iterable[A, Repr]的方法,實現如下:

def map[B, That](f: (A) => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
    val builder = bf()
    val iter = iterator()
    while (iter.hasNext) builder += f(iter.next())
    builder.result
}

這裡Repr的意思是 “展現型別” 。該引數將讓我們選擇合適的構建器工廠來構建諸如Range或String這樣的非常規集合。

CanBuildFrom[From, E, To]特質將提供型別證明,可以建立一個型別為To的集合,握有型別為E的值,並且和型別From相容。

CanBuildFrom特質帶有一個apply方法,產出型別為Builder[E, To]的物件。Builder型別帶有一個+=方法用來將元素新增到一個內部的緩衝,還有一個result方法用來產出鎖要求的集合。

trait Builder[-E, +To] {
    def +=(e: E): Unit
    def result(): To
}

trait CanBuildFrom[-From, -E, +To] {
    def apply(): Builder[E, To]
}

因此,map方法只是構造出一個目標型別的構建器,為構建器填充函式f的值,然後產出結果的集合。

每個集合都在其伴生物件中提供了一個隱式的CanBuildFrom物件。考慮如下簡化版的ArrayBuffer類:

class Buffer[E: Manifest] extends Iterable[E, Buffer[E]] with Builder[E, Buffer[E]] {
    private var elems = new Array[E](10)
    ...
    def iterator = { 
        ...
        private var i = 0
        def hasNext = i < length
        def next() = { i += 1; elems(i - 1) }
    }
    def +=(e: E) { ... }
    def result() = this
}

object Buffer {
    implicit def canBuildFrom(E: Manifest] 
      = new CanBuildFrom[Buffer[_], E, Buffer[E]] {
        def apply() = new Buffer[E]
    }
}

看看如果呼叫buffer.map(f)會發生什麼,其中f是一個型別為A => B的函式。首先,通過呼叫Buffer伴生物件中的canBuildFrom[B]方法,可以得到隱式的bf引數。它的apply方法返回了構建器,即Buffer[E]。

由於Buffer類碰巧已經有一個+=方法,而它的result方法也被定義為返回它自己。因此,Buffer就是它自己的構建器。

然而,Range類的構建器並不返回一個Range,而且它顯然也不能返回Range。舉例來說,(1 to 10).map(x => x * x)的結果並不是一個Range。在實際的Scala類庫中,Range擴充套件自IndexedSeq[Int],而IndexedSeq的伴生物件定義了一個構建Vector的構建器。

以下是一個簡化版的Range類,提供了一個Buffer作為其構建器:

class Range(val low: Int, val high: Int) extends Iterable[Int, Range] {
    def iterator() = ...
}

object Range {
    implicit def canBuildFrom[E: Manifest] 
      = new CanBuildFrom[Range, E, Buffer[E]] {
        def apply() = new Buffer[E]
    }
}

如下呼叫:Rang(1, 10).map(f)。這個方法需要一個implicit bf: CanBuildFrom[Repr, B, That]。由於Repr就是Range,因此相關聯的型別有CanBuildFrom、Range、B和未知的That。其中Range物件可以通過呼叫器canBuildFrom[B]方法產出一個匹配項,該方法返回一個CanBuildFrom[Range, B, Buffer[B]]。這個匹配項就成為bf;其中apply方法將產出Buffer[B],用於構建結果。

正如剛才看到的,隱式引數CanBuildFrom[Repr, B, That]將會定位到一個可以產出目標集合的構建器的工廠物件。這個構建器工廠是定義在Repr伴生物件中的一個隱式值。

21.11 習題解答


1. ->的工作原理是什麼?或者說,”Hello” -> 42和42 -> “Hello”怎麼會和對偶(“Hello”, 42)和(42, “Hello”)扯上關係呢?提示:Predef.any2ArrowAssoc


2. 定義一個操作符+%,將一個給定的百分比新增到某個值。舉例來說,120 +% 10應得到132。提示:由於操作符的方法,而不是函式,你需要提供一個implicit。


3. 定義一個!操作符,計算某個整數的階乘。舉例來說,5!應得到120。你將會需要一個經過豐富的類和一個隱式轉換。


4. 有些人很喜歡那些讀起來隱約像英語句子的 “流利API”。建立一個這樣的API,用來從控制檯讀取整數、浮點數以及字串。例如:

Read in aString askingFor "Your name" and anInt askingFor "Your age" and aDouble askingFor "Your weight"


5. 提供執行21.6節中的下述運算所需要的程式碼:

smaller(Fraction(1, 7), Fraction(2, 9))

給出一個擴充套件自Ordered[Fraction]的RichFraction類。


6. 比較java.awt.Point類的物件,按詞典順序比較(即依次比較x座標和y座標的值)。


7. 繼續前一個練習,根據兩個點到原點的距離進行比較。你如何在兩種排序之間切換?


8. 在REPL中使用implicitly命令來召喚出21.5節及21.6節中的隱式物件。你得到了哪些物件?


9. 在Predef.scala中查詢=:=物件。解釋它的工作原理。


10. 表示式"abc".map(_toUpper)的結果是一個String,但"abc".map(_toInt)的結果是一個Vector。搞清楚為什麼會這樣。

第22章 定界延續

22.1 捕獲並執行延續

延續是這樣一種機制,它讓你回到程式中之前的一個點。

首先,使用shift結構捕獲一個延續。在shift當中,你必須講明當一個延續被交個你之後,你想要做些什麼事。

var cont: (Unit => Unit) = null
...
shift { k: (Unit => Unit) => // 延續被傳遞給了shift
    cont = k // 儲存下來,以便之後能使用
}

在Scala中,延續是定界的——它只能延展到給定的邊界。這個邊界由reset { … } 標出:

reset {
    ...
    shift { k: (Unit => Unit) =>
        cont = k
    } // 對cont的呼叫將從此處開始...
    ...
} // ...到此處結束

當你呼叫cont時,執行將從shift處開始,並一直延展到reset塊的邊界。

以下是一個完整的示例。將讀取一個檔案並捕獲延續。

var cont: (Unit => Unit) = null
var filename = "myfile.txt"
var contents = ""

reset {
    while (contents == "") {
        try {
            contents = scala.io.Source.fromFile(filename, "UTF-8").mkString
        } catch { case _ => }
        shift { k: (Unit => Unit) =>
            cont = k
        }
    }
}

// 要重試的話,只需要執行延續即可:
if (contents == "") {
    print("Try another filename: ")
    filename = readLine()
    cont() // 跳回到shift
}
println(contents)

注:在Scala2.9中,需要啟動延續外掛才能編譯使用延續的程式:

scalac -P:continuations:enable MyProg.scala

22.2 “運算當中挖個洞”

要理解到底一個延續捕獲了什麼,我們可以把shift塊想象成一個位於reset塊中的 “洞”。當你執行延續時,你可以將一個值傳到這個洞中,運算繼續,就好像shift本就是那個值一樣。

22.3 reset和shift的控制流轉

reset/shift有雙重職責——一方面定義延續函式,另一方面又捕獲延續函式。

當你呼叫reset時,它的程式碼體便開始執行。當執行遇到shift時,shift的程式碼體被呼叫,傳入延續函式作為引數。當shift完成後,執行立即跳轉到包含延續的reset塊的末尾。

var cont: (Unit => Unit) = null
reset {
    println("Before shift")
    shift {
        k: (Unit => Unit) => {
            cont = k
            println("Insert shift") // 跳轉到reset末尾
        }
    }
    println("After shift")
}
println("After reset")
cont()

// 當reset執行時,上述程式碼將列印
Before shift
Inside shift

// 然後它將退出reset塊並列印
After reset

// 最後,當呼叫cont時,執行跳回到reset塊,並打印出
After shift

22.4 reset表示式的值

如果reset塊退出時因為由於執行了shift,那麼得到的值就是shift塊的值:

val result = reset { shift { k: (String => String) => "Exit" }; "End" }

// result為“Exit”

如果reset塊執行到自己的末尾的話,它的值就只是reset塊的值——亦即塊中最後一個表示式的值:

val result = reset { if (false) shift { k: (String => String) => "Exit" }; "End" }

// result為“End”

22.5 reset和shift表示式的型別

reset和shift都是帶有型別引數的方法,分別是reset[B, C]和shift[A, B, C]。

reset {
    shift前
    shift { k: (A => B) => // 由此處推斷A和B
        shift中 // 型別C
    } // "洞"的型別為A
    shift後 // 必須產出型別B的值
}

這裡的A是延續的引數型別——“被填入洞中” 的值的型別。B是延續的返回型別——當有人執行延續的時候返回的值的型別。C是預期的reset表示式的型別——亦即從shift返回的值的型別。(如果reset塊有可能返回一個型別為B或C的值,那麼B必須是C的子型別。)

注意這些型別是很重要的。如果編譯器無法正確地推斷出它們,它將報告一個很隱晦的錯誤提示。

22.6 CPS註解

在某些虛擬機器中,延續的實現方式是抓取執行期棧的快照。當有人呼叫延續時,執行期棧被恢復成快照的樣子。可惜Java虛擬機器並不允許進行這樣的操作。為了在JVM中提供延續,Scala編譯器將對reset塊中的程式碼進行 “延續傳遞風格”(CPS)的變換。

經過變換的程式碼與常規的Scala程式碼很不一樣,而且你不能混用這兩種風格。對於方法而言,這個區別尤為明顯。如果方法包含shift,那它將不會被編譯成常規的方法。你必須將它註解為一個 “被變換” 的方法。

上一節中的shift有三個型別引數——分別是延續函式的引數和返回型別,以及程式碼塊的型別。要呼叫一個包含了shift[A, B, C]的方法,它必須被註解為@cpsParam[B, C]。在通常的情況,即B和C相同時,可以用註解@cps[B]。

def tryRead(): Unit @cps[Unit] = {
    while (contents == "") {
        try {
            contents = scala.io.Source.fromFile(filename, "UTF-8").mkString
        } catch { case _ => }
        shift { k: (Unit => Unit) =>
            cont = k
        }
    }
}

如果某方法呼叫帶有@cps註解的方法,而該呼叫本身又不位於reset程式碼塊的話,則該方法也必須被加上註解。換句話說,任何位於reset和shift之間的方法都必須加上註解。

22.7 將遞迴訪問轉化為迭代

例如,如下方法將列印給定目錄中所有子目錄的所有檔案:

def processDirectory(dir: File) {
    val files = dir.listFiles
    for (f <- files) {
        if (f.isDirectory)
            processDirectory(f)
        else 
            println(f)
    }
}

如果只是看前100個檔案的話,沒法在當中停掉遞迴。而用延續的話就簡單了。每發現一個節點,我們就跳出遞迴。如果我們需要更多結果,我們就跳回去。

reset和shift方法尤其適合這種控制流轉的模式。每當shift被執行,程式就退出包含它的reset。當被捕獲的延續被呼叫時,程式又再返回shift的位置。

要實現這個設計,可以在訪問應被中斷的點上放置一個shift。

if (f.isDirectory)
    processDirectory(f)
else {
     shift {
         k: (Unit => Unit) => {
             cont = k
         }
    }
    println(f)
}

這裡的shift承擔兩個職責。每當它被執行,它就會跳到reset的末尾,同時它會捕獲到延續,這樣我們才能回得來。

將整個過程的啟動點用reset包起來,然後就可以以我們想要呼叫的次數來呼叫捕獲到延續了:

reset {
    processDirectory(new File(rootDirName))
}
for (i <- 1 to 100) cont()

當然了,processDirectory方法需要一個CPS註解:

def processDirectory(dir: File): Unit @cps[Unit]

for迴圈會被翻譯成一個對foreach的呼叫,而foreach並沒有被註解為CPS,因此我們無法呼叫它。簡單地改為while迴圈就好:

var i = 0;
while (i < files.length) {
    val f = files(i)
    i += 1
    ...
}

以下是完整的程式:

import java.io.File

import scala.util.continuations._

/**
  * Created by zhangws on 17/2/15.
  */
object C22_7 {

    var cont: (Unit => Unit) = null

    def processDirectory(dir: File): Unit@cps[Unit] = {
        val files = dir.listFiles
        var i = 0
        while (i < files.length) {
            val f = files(i)
            i += 1
            if (f.isDirectory) {
                processDirectory(f)
            } else {
                shift {
                    k: (Unit => Unit) => {
                        cont = k // 2
                    }
                } // 5
                println(f)
            }
        }
    }

    def main(args: Array[String]) {
        reset {
            processDirectory(new File("/")) // 1.
        } // 3
        for (i <- 1 to 100) cont() // 4
    }
}

在進入reset塊時,processDirectory方法被呼叫1。一旦該方法找到第一個不是目錄的檔案,它將進入shift2。延續函式被儲存到cont,程式跳到reset塊的末尾3。

接下來,cont被呼叫4,程式重新跳回遞迴5,進入到 “shift洞” 中。遞迴繼續,直到下一個檔案被找到,再次進入shift。在shift的末尾,程式將跳到reset的末尾,然後cont函式返回。

22.8 撤銷控制反轉

一個很有前景的延續應用是撤銷GUI或Web程式設計中的 “控制反轉”。

示例如下:

在你傳送完第一個頁面給使用者之後,你的程式就處於等待狀態。最後當用戶的響應抵達時,它必須被路由到傳送第二個頁面的那部分程式邏輯。當用戶對第二個頁面做出響應後,相關處理又會在另一個不同的地方發生。

基於延續的Web框架能夠解決這個問題。當應用做出一個網頁並等待使用者響應時,一個延續會被保留下來。當用戶響應抵達後,這個延續會被呼叫。

import java.awt.BorderLayout
import java.awt.event.{ActionEvent, ActionListener}
import javax.swing._

import scala.util.continuations._

/**
  * Created by zhangws on 17/2/15.
  */
object C22_8 extends App {

    val frame = new JFrame
    val button = new JButton("Next")
    setListener(button) {
        run()
    }
    val textField = new JTextArea(10, 40)
    val label = new JLabel("Welcome to the demo app")

    frame.add(label, BorderLayout.NORTH)
    frame.add(textField)

    val panel = new JPanel
    panel.add(button)
    frame.add(panel, BorderLayout.SOUTH)
    frame.pack()
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
    frame.setVisible(true)

    def run(): Unit = {
        reset {
            val response1 = getResponse("What is your first name?")
            val response2 = getResponse("what is your last name?")
            process(response1, response2)
        }
    }

    def process(s1: String, s2: String): Unit = {
        label.setText("Hello, " + s1 + " " + s2)
    }

    var cont: Unit => Unit = null

    def getResponse(prompt: String): String@cps[Unit] = {
        label.setText(prompt)
        setListener(button) {
            cont()
        }
        shift {
            k: (Unit => Unit) => {
                cont = k
            }
        }
        setListener(button) {}
        textField.getText
    }

    def setListener(button: JButton)(action: => Unit): Unit = {
        for (l <- button.getActionListeners) button.removeActionListener(l)
        button.addActionListener(new ActionListener {
            override def actionPerformed(e: ActionEvent): Unit = {
                action
            }
        })
    }
}

22.9 CPS變換

CPS變換會產出一些物件,這些物件會指定如何處理包含 “餘下的運算” 的函式。如下的shift方法:

shift { 函式 }

// 返回一個物件
ControlContext[A, B, C](函式)

shift的程式碼體是一個型別為(A => B) => C的函式。它接受型別為A => B的延續作為引數,產出一個型別為C的值,該值被傳遞到包含延續的reset塊之外。

一個控制上下文(ControlContext)描述瞭如何處理延續函式。通常,它會將函式推到一邊,有時候它也會計算出某個值。

控制上下文並不知道如何計算延續函式。它有賴於別人來計算它。它只是預期接收延期而已。

shift被翻譯成一個知道如何處理shift之後所有事情的控制上下文。

new ControlContext(k1 => fun(a => k1(f(a))))

這裡的a => k1(f(a))首先執行f,然後再完成有k1指定的運算。而fun則按照通常的方式處理運算結果。

這種 “緩慢前行” 是控制上下文的基本操作,它被稱做map。以下是map方法的定義:

class ControlContext[+A, -B, +C](val fun: (A => B) => C) {
    def map[A1](f: A => A1) = new ControlContext[A1, B, C](
        (k1: (A1 => B)) => fun(x: A => k1(f(x))))
    ...
}

cc.map(f)接受一個控制上下文,將它變成一個能處理f之後餘下的運算的控制上下文。

reset正是如此定義的:

def reset[B, C](cc: ControlContext[B, B, C]) = cc.fun(x => x)

先看一個簡單的示例:

reset {
    0.5 * { shift { k: (Int => Double) => cont = k } } + 1
}

拿本例來說,編譯器可以用一步就計算出整個延續。即:

=> 0.5 * □ + 1

因此,我們得到

reset {
    new ControlContext[Int, Double, Unit](k => cont = k)
        .map(□ => 0.5 * □ + 1)
}

亦即:

reset {
    new ControlContext[Double, Double, Unit](k1 => 
        cont = k1(x: Int => 0.5 * x + 1)
}

這樣,reset就可以被求值了,k1是一個恆等函式,結果為:

cont = x: Int => 0.5 * x + 1

呼叫reset只是設定cont,並沒有其他效果。


注:如果用-Xprint:selectivecps編譯器標誌編譯,就可以看到CPS變換生成的程式碼。

22.10 轉換巢狀的控制上下文

示例:看看如何將一個遞迴的訪問轉化為迭代。簡單起見,我們將訪問一個連結串列,而不是一棵樹:

def visit(a: List[String]): String @cps[String] = {
    if (a.isEmpty) "" else {
        shift {
            k: (Unit => String) => {
                cont = k
                a.head
            }
        }
        visit(a.tail)
    }
}

和之前一樣,shift被轉換成控制上下文:

new ControlContext[Unit, String, String](k => { cont = k; a.head })

不過這一次在shift之後還跟著一個對visit的呼叫,該呼叫返回另一個ControlContext

更確切地說,shift被替換成了(),因為延續函式的引數型別為Unit。這樣,餘下的運算就是:

() => visit(a.tail)

沿著第一個示例的思路,我們會把這個函式作為引數呼叫map。但由於它返回的是一個控制上下文,因此我們用flatMap:

if (a.isEmpty) new ControlContext(k => k(")) else 
    new ControlContext(k => { cont = k; a.head })
        .flatMap(() => visit(a.tail))

以下是flatMap的定義:

class ControlContext[+A, -B, +C](val fun: (A => B) => C) {
    ...
    def flatMap[A1, B1, C1 <:B](f: A => Shift[A1, B1, C1]) =
        new ControlContext[A1, B1, C](
            (k1: (A1 => B1)) => fun(x: A => f(x).fun(k1)))
}

// 上面程式碼的意思是:如果餘下的運算是以另一個想要處理餘下運算的餘下部分的控制上下文開始的話,讓它做。
// 這將定義出一個延續,由我們來處理。

我們來模擬一次呼叫:

val lst = List("Fred")
reset { visit(lst) }

由於lst不是空的,我們得到:

reset {
    new ControlContext(k => { cont = k; lst.head }).flatMap(() => visit(lst.tail))
}

根據flatMap的定義,我們得到:

reset {
    new ControlContext(k => { cont = () => visit(lst.tail).fun(k1); lst.head })
}

然後reset將k1設為恆等函式,我們將得到:

cont = () => visit(lst.tail).fun(x => x)
lst.head

現在我們呼叫cont。如果我們用了更長的列表,lst.tail不會是空的,於是我們再次得到同樣的結果,不過這一次是visit(lst.tail.tail)。但由於我們已經把列表遍歷完了,visit(lst.tail)將返回

new ControlContext(k => k(""))

應用恆等函式,得到結果”“。

22.11 習題解答


1. 在22.1節的示例當中,假定並不存在檔案myfile.txt。現在把filename設為另一個不存在的檔案並呼叫cont。會發生什麼?將filename設為一個存在的檔案並再次呼叫cont。會發生什麼?再多呼叫一次cont。會發生什麼?首先,在腦海裡過一遍控制流轉,然後執行程式來驗證你的想法。


2. 改進22.1節的示例,讓延續函式將下一個需要嘗試的檔案的名稱作為引數傳遞。


3. 將22.7節中的例項改成迭代器。迭代器的構造器應包含reset,而next方法應執行延續。


4. 22.8節的示例程式碼看上去並不美觀——應用程式開發人員能看到reset語句。將reset從run方法移到按鈕監聽器中。現在應用程式開發人員是不是很愜意地不知道延續的存在了呢?


5. 考慮如下示例程式,它使用延續將迭代轉化成迭代器:

object Main extends App {
    var cont: Unit => String = null
    val a = "mary has a little lamb".split(" ")
    reset {
        var i = 0
        while (i < a.length) {
            shift {
                k: (Unit => String) => {
                    cont = k
                    a(i)
                }
            }
            i += 1
        }
        ""
    }
    println(cont())
    println(cont())
}

-Xprint:selectivecps標誌編譯並檢視生成的程式碼。經過CPS變換的while語句是什麼樣子的?