英文出自:yehudakatz.com,編譯:CSDN趙紅
常常會遇到有人將Ruby的區塊(Blocks)看作相當於JavaScript的“first class functions ”的誤解。由於傳遞功能,尤其是當你可以建立匿名的傳遞功能,這是非常強大的。事實上,JavaScript和Ruby有一個機制使其自然會認為等值。
人們在談到為什麼Ruby的區塊不同於Python的函式時,通常會講到一些關於Ruby和JavaScript的匿名分享,但Python沒有。初看之下,一個Ruby區塊就是一個“匿名函式”(或俗稱一個“封裝”),正如JavaScript函式就是其中之一。
作為一個早期的Ruby/JavaScript開發者,無可否認我也有過這樣的觀點分享。錯過一個重要的細節,對結果會產生較大影響。這個原理常被稱為“Tennent’s Correspondence Principle”,這條原理說:“For a given expression expr, lambda expr should be equivalent.”這就是被稱為抽象的原則,因為這意味著,用“區塊”的方法很容易重構通用程式碼。例如,常見檔案資源管理的情況。試想在Ruby中,File.open塊形式是不存在的,你會看到以下程式碼:
1 2 3 4 5 6 7 |
begin f = File.open(filename, "r") # do something with f ensure f.close end |
1 |
在一般情況下,“區塊”程式碼有著相同的開始和結束編碼、不同的內部編碼。現在重構這段程式碼,你會這樣寫: |
1 2 3 4 5 6 |
def read_file(filename) f = File.open(filename, "r") yield f ensure f.close end |
1 |
程式碼中的模式與重構例項: |
1 2 3 |
read_file(filename) do |f| # do something with f End |
1 |
重要的是重構之後區塊內的程式碼和以前一樣 。在以下情況時,我們可以重申抽象原則的對應原理: |
1 |
# do something with f |
1 |
應相等於: |
1 2 3 |
do # do something with end |
乍一看,在Ruby和JavaScript中確實如此。例如,假設你正在使用的檔案列印它的mtime。您可以輕鬆地重構相當於在JavaScript:
1 2 3 4 5 6 7 |
try { // imaginary JS file API var f = File.open(filename, "r"); sys.print(f.mtime); } finally { f.close(); } |
到這裡:
1 |
read_file(function(f) { sys.print(f.mtime); }); |
事實上,這樣的情況往往給人錯誤的印象,Ruby和JavaScript有同樣用匿名函式重構常用功能的能力。
不過,再來一個稍微複雜一些的例子。我們首先在Ruby中編寫一個簡單的類,計算檔案的mtime和檢索它的正文:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class FileInfo def initialize(filename) @name = filename end # calculate the File's +mtime+ def mtime f = File.open(@name, "r") mtime = mtime_for(f) return "too old" if mtime < (Time.now - 1000) puts "recent!" mtime ensure f.close end # retrieve that file's +body+ def body f = File.open(@name, "r") f.read ensure f.close end # a helper method to retrieve the mtime of a file def mtime_for(f) File.mtime(f) end end |
1 |
我們可以用區塊很容易地重構這段程式碼: |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class FileInfo def initialize(filename) @name = filename end # refactor the common file management code into a method # that takes a block def mtime with_file do |f| mtime = mtime_for(f) return "too old" if mtime < (Time.now - 1000) puts "recent!" mtime end end def body with_file { |f| f.read } end def mtime_for(f) File.mtime(f) end private # this method opens a file, calls a block with it, and # ensures that the file is closed once the block has # finished executing. def with_file f = File.open(@name, "r") yield f ensure f.close end end |
同樣地,需要注意的重點是,我們構建區塊卻並不改變它的內部程式碼。但不幸的是,這個相同情況的例子無法在JavaScript中正常工作。讓我們在JavaScript中來寫等同的FileInfo類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// constructor for the FileInfo class FileInfo = function(filename) { this.name = filename; }; FileInfo.prototype = { // retrieve the file's mtime mtime: function() { try { var f = File.open(this.name, "r"); var mtime = this.mtimeFor(f); if (mtime < new Date() - 1000) { return "too old"; } sys.print(mtime); } finally { f.close(); } }, // retrieve the file's body body: function() { try { var f = File.open(this.name, "r"); return f.read(); } finally { f.close(); } }, // a helper method to retrieve the mtime of a file mtimeFor: function(f) { return File.mtime(f); } }; |
如果我們試圖將其轉換成一個接受重複函式的程式碼,那mtime方法看起來將是:在這裡有兩個非常普遍的問題。首先是上下文改變了。我們可以通過允許繫結第二引數,但這意味著每次重構時需要確認並通過一個變數傳遞引數,就是說這一情況會在因為缺乏JavaScript信任元件時而出現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function() { // refactor the common file management code into a method // that takes a block this.withFile(function(f) { var mtime = this.mtimeFor(f); if (mtime < new Date() - 1000) { return "too old"; } sys.print(mtime); }); } |
這很煩人,更棘手的還在於,它是從內部返回結果而不是從函式外部。這個真實的結果違反了抽象原則中的對應原理。相反,在函式中用區塊方法毫不費力地重構具有相同開始和結束的程式碼時,JavaScript庫作者需要考慮使用者對API處理巢狀函式時進行的一些操作。作為一個JavaScript庫資源的編寫者和使用者看來,這提供了一個很好的基於區塊的API。
迭代和回撥
值得注意的是,區塊lambda函式接受功能呼叫的案例包括迭代器、同步與互斥、資源管理(如區塊形式的File.open)。
使用函式作為回撥時,關鍵字不再有意義。從一個已經返回的函式返回是什麼意思?在這種情況下,通常涉及回撥函式lambda表示式做出了很大的意義。在我看來,這解釋了為什麼JavaScript事件觸發程式碼,涉及了大量的回撥。
由於這些問題,ECMA工作組負責的ECMAScript,TC39,正在考慮加入塊lambda表示式語言。這將意味著,上面的例子可重構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
FileInfo = function(name) { this.name = name; }; FileInfo.prototype = { mtime: function() { // use the proposed block syntax, `{ |args| }`. this.withFile { |f| // in block lambdas, +this+ is unchanged var mtime = this.mtimeFor(f); if (mtime < new Date() - 1000) { // block lambdas return from their nearest function return "too old"; } sys.print(mtime); } }, body: function() { this.withFile { |f| f.read(); } }, mtimeFor: function(f) { return File.mtime(f); }, withFile: function(block) { try { var f = File.open(this.name, "r"); block(f); } finally { f.close(); } } }; |
TC39並沒有實質性改變這個例子,並且要注意區塊lambda表示式自動返回他們的最後一個語句。
經驗顯示,Smalltalk和Ruby不需要理解一種語言可怕的對應原理,滿足它獲得自己想要的結果。“迭代”概念並不內建到語言,而是被自然塊定義的結果。這使得Ruby以及其他常用語言的開發者可建立自定義的豐富、內建的迭代設定。作為一個JavaScript實踐者,我經常碰到的情況是,用一個for迴圈比使用forEach更為簡單明瞭。
業界人士觀點
In order to have a language with return (and possibly super and other similar keywords) that satisfies the correspondence principle, the language must, like Ruby and Smalltalk before it, have a function lambda and a block lambda. Keywords like return always return from the function lambda, even inside of block lambdas nested inside.In case you want to get your google/wikipedia on, what Katz is talking about here is a “non-local return”.
更多評論詳細請點選這裡>>
ericbb :Alternate formulation with hypothetical shift/reset (delimited continuation support) and blocks that return
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mtime: function () { return reset { var mtime = shift (succeed) { this.withFile ({ |f| var mtime = this.mtimeFor (f); if (mtime < new Date () - 1000) { return "too old"; } return succeed (mtime); }); }; sys.print (mtime); return "young enough"; }; }, The succeed function is a first class, indefinite-extent function equivalent to: function (mtime) { sys.print (mtime); return "young enough"; } |
It’s the computation within the reset block that comes after the shift form is evaluated。Calling it after returning from the method would not raise an exception.
更多評論詳細請點選這裡>>