JavaScript中的持續傳遞風格


JavaScript中的持續傳遞風格

持續傳遞風格( CPS)是起源於20世紀70年代的一種編程風格,它在20世紀80和90年代成為高級程序設計語言中的特色之一。

現在,CPS作為非阻塞式(通常是分佈式的)系統的編程風格而被再次發掘出來。

我對CPS很有好感,因為它是我獲取博士學位的一個秘密武器。它十有八九幫我消減掉了一兩年的時間和一些難以估量的痛苦。

本文介紹了CPS所扮演的兩種角色作為JavaScript中的一種非阻塞編程風格,以及作為一種功能性語言的中間形式(簡要介紹)。

內容包括:

  • JavaScript中的CPS
  • CPS用於Ajax編程
  • 用在非阻塞式編程(node.js)中的CPS
  • CPS用於分佈式編程
  • 如何使用CPS來實現異常
  • 極簡Lisp的一個CPS轉換器
  • 如何用Lisp實現call/cc
  • 如何用JavaScript實現call/cc

什麼是持續傳遞風格?

如果一種語言支持持續傳遞(continuation)的話,編程者就可以添加諸如異常、回溯、線程以及構造函數一類的控制構造。

可惜的是,許多關於持續的解釋(我的也包括在內)給人的感覺是含糊不清,令人難以滿意。

持續傳遞風格是那麼的基礎。

持續傳遞風格賦予了後續在代碼方面的意義。

更妙的是,編程者可以自我發掘出持續傳遞風格來,如果其受限於下面這樣的一個約束的話:

沒有任何過程可以返回給他的調用者。

一個提示使得這種風格的編程成為可能:

過程可以在它們返回值時調用一個回調方法。

當一個過程(procedure)準備要返回到它的調用者中時,它在返回值時調用當前後續(current continuation)這一回調方法(由它的調用者提供)。

一個後續是一個初始類型(first-class)返回點。

例子:標識函數

考慮這個正常寫法的標識函數:


JavaScript中的持續傳遞風格


然後是持續傳遞風格的:


JavaScript中的持續傳遞風格


有時候,把當前後續參數命名為ret會使得其目的更為明顯一些:


JavaScript中的持續傳遞風格


例子:樸素階乘

下面是標準的樸素階乘:


JavaScript中的持續傳遞風格


下面是CPS風格實現的:


JavaScript中的持續傳遞風格


接下來,為了使用這一函數,我們把一個回調方法傳給它:


JavaScript中的持續傳遞風格


例子:尾遞歸階乘

下面是尾遞歸階乘:


JavaScript中的持續傳遞風格


然後,是CPS實現:


JavaScript中的持續傳遞風格


CPS和Ajax

Ajax是一種web編程技術,其使用JavaScript中的一個XMLHttpRequest對象來從服務器端(異步地)提取數據。(提取的數據不必是XML格式的。)

CPS提供了一種優雅地實現Ajax編程的方式。

使用XMLHttpRequest,我們可以寫出一個阻塞式的過程fetch(url),該過程抓取某個url上的內容,然後把內容作為串返回。

這一方法的問題是,JavaScript是一種單線程語言,當JavaScript阻塞時,瀏覽器就被暫時凍結,不能動彈了。這會造成不愉快的用戶體驗。

一種更好的做法是這樣的一個過程fetch(url, callback),其允許執行(或是瀏覽器呈現工作)的繼續,並且一旦請求完成就調用所提供的回調方法。

在這種做法中,部分CPS轉換變成了一種自然的編碼方式。

實現fetch

實現fetch過程並不難,至於其以非阻塞模式或是阻塞模式操作則取決於編程者是否提供回調方法:


JavaScript中的持續傳遞風格


例子:提取數據

考慮一個程序,該程序需要從UID中抓取一個名字

下面的兩種做法都要用到fetch:


JavaScript中的持續傳遞風格


JavaScript中的持續傳遞風格


CPS和非阻塞式編程

node.js是一個高性能的JavaScript服務器端平臺,在該平臺上阻塞式過程是不允許的。

巧妙的是,通常會阻塞的過程(比如網絡或是文件I/O)利用了通過結果來調用的回調方法。

對程序做部分CPS轉換促成了自然而然的node.js編程。

例子:簡單的web服務器

node.js中的一個簡單的web服務器把一個持續傳遞給文件讀取過程。相比於非阻塞式IO的基於select的方法,CPS使非阻塞I/O變得更加的簡單明瞭。


JavaScript中的持續傳遞風格


CPS用於分佈式計算

CPS簡化了把計算分解成本地部分和分佈部分的做法。

假設你編寫了一個組合的choose函數;開始是一種正常的方式:


JavaScript中的持續傳遞風格


現在,假設你想要在服務器端而不是本地計算階乘。

你可以重新把fact寫成阻塞的並等待服務器端的響應。

那樣的做法很糟糕。

相反,假設你使用CPS來寫choose的話:


JavaScript中的持續傳遞風格


現在,重新把fact定義成在服務器端的異步計算階乘就是一件很簡單的事情了。

(有趣的練習:修改node.js服務器端以讓這一做法生效。)

使用CPS來實現異常

一旦程序以CPS風格實現,其就破壞了語言中的普通的異常機制。 幸運的是,使用CPS來實現異常是一件很容易的事情。

異常是後續的一種特例。

通過把當前異常後續(current exceptional continuation)與當前後續一起做傳遞,你可以實現對try/catch代碼塊的脫糖處理。

考慮下面的例子,該例子使用異常來定義階乘的一個完全版本:


JavaScript中的持續傳遞風格


通過使用CPS來添加異常後續,我們就可以對throw、try和catch做脫糖處理:


JavaScript中的持續傳遞風格


CPS用於編譯

三十年以來,CPS已經成為了功能性編程語言的編譯器的一種強大的中間表達形式。

CPS脫糖處理了函數的返回、異常和初始類型後續;函數調用變成了單條的跳轉指令。

換句話說,CPS在編譯方面做了許多繁重的提升工作。

把lambda演算轉寫成CPS

lambda演算是Lisp的一個縮影,只需足夠的表達式(應用程序、匿名函數和變量引用)來使得其對於計算是通用的。


JavaScript中的持續傳遞風格


下面的Racket代碼把這一語言轉換成CPS:


JavaScript中的持續傳遞風格


對於感興趣的讀者來說,Olivier Danvy有許多關於編寫有效的CPS轉換器的文章。

使用Lisp實現call/cc

原語call-with-current-continuation(通常稱作call/cc)是現代編程中最強大的控制流結構。

CPS使得call/cc的實現成為了小菜一碟;這是一種語法上的脫糖:


JavaScript中的持續傳遞風格


這一脫糖處理(與CPS轉換相結合)是準確理解call/cc所做工作的最好方式。

其所實現的正是其名稱所說明的:其使用一個已經捕捉了當前後續的過程來調用被作為參數指定的過程。

當捕捉了後續的過程被調用時,其把計算返回給計算創建點。

使用JavaScript實現call/cc

如果有人要把JavaScript中的代碼轉寫成持續傳遞風格的話,call/cc有一個很簡單的定義:


JavaScript中的持續傳遞風格


十五年編程經驗,今年1月整理了一批2019年最新WEB前端教學視頻,不論是零基礎想要學習前端還是學完在工作想要提升自己,這些資料都會給你帶來幫助,從HTML到各種框架,幫助所有想要學好前端的同學,學習規劃、學習路線、學習資料、問題解答。只要關注我的頭條號,後臺私信我【前端】兩個字,即可免費獲取。


分享到:


相關文章: