過濾器資源和匯入點 |
|
作者:DiegoNehab
某些運算可以實作為過濾器。過濾器是一個處理依序函式呼叫中接收資料的函式,一塊一塊回傳部分結果。可以實作為過濾器的運算範例包括文字的換行符號正規化、Base64 和 Quoted-Printable 傳輸內容編碼、文字換行、SMTP 位元填充,還有很多其他範例。當我們允許將過濾器串接在一起以建立複合式過濾器時,過濾器變得更強大。過濾器可以視為資料轉換鏈中的中間節點。資源和匯入點是這些鏈中的對應端點。資源是一個產生資料的函式,一塊一塊產生,而匯入點是一個接收資料的函式,一塊一塊接收。在本技術備忘中,我們定義一個優雅的介面作為過濾器、資源、匯入點和串接。我們逐步延伸我們的介面,直到達到高度的通用性。我們討論實作此介面時發生的困難,並提供解決方案和範例。
有時應用程式有太多要處理的資訊,導致無法放入記憶體,因此被迫以較小的部分處理資料。即使有足夠的記憶體,一次性處理所有資料可能需要很長的時間,會讓想要與應用程式互動的使用者感到沮喪。此外,複雜的轉換通常可以定義為一系列較簡單的運算。許多不同的複雜轉換可能共用相同的較簡單運算,因此需要一個統一的介面來組合它們。下列概念構成我們對這些問題的解決方案。
過濾器是接受輸入中連續區塊的函式,並產生連續區塊的輸出。此外,串接所有輸出資料的結果與針對輸入資料串接進行過濾的結果相同。因此,界線無關緊要:過濾器必須處理使用者任意分割的輸入資料。
鏈是一個函式,它結合兩個(或更多)其他函式的效果,但其介面與其其中一個元件的介面無異。因此,串接式過濾器可用於原子過濾器可用之處。但是,它對資料的影響是其元件過濾器的綜合影響。請注意,因此,鏈本身可以串接,以建立可以像原子運算一樣使用的任意複雜運算。
篩選器可視為網路中的內部節點,資料會透過此網路節點流動,而且資料在流動的過程中可能會轉換。鏈串將這些節點串接起來。若要完成這幅圖像,我們還需要來源和匯入分別當作網路的起始和結束節點。以較不抽象的方式來說,來源是每次呼叫時都會產生新資料的函式。另一方面,匯入則是提供收到的資料最終目的地的函式。自然地,來源和匯入可以與篩選器鏈接在一起。
最後,篩選器、鏈串、來源和匯入全都是被動實體:它們需要重複呼叫才能讓某件事發生。泵浦提供推進力,將資料從來源推送到匯入,讓資料在網路中流動。
希望這些概念透過範例能變得更加明瞭。在以下各節中,我們從簡化的介面開始,我們對這個介面進行多次改善,直到我們再也找不到明顯的缺點為止。我們提出的演算法並非強求:它遵循我們在彙整這些概念的理解時所遵循的步驟。
有些資料轉換比其他轉換更容易實作為篩選器。可以實作為篩選器的操作範例包括文字的結尾正常化、Base64 和 Quoted-Printable 傳輸內容編碼、將文字分行、SMTP 位元填塞等,而且還有許多其他範例。我們使用結尾正常化作為範例,定義我們的初始篩選器介面。稍後我們會討論為什麼實作可能不會很簡單。
假設給定一個具有不明結尾慣例的文字 (包括可能的混合慣例) 而未說明是否為 Unix (LF)、Mac OS (CR) 以及 DOS (CRLF) 慣例。我們希望能夠撰寫像以下這樣的程式碼
input = source.chain(source.file(io.stdin), normalize("\r\n")) output = sink.file(io.stdout) pump(input, output)
這個程式應該從標準輸入串流讀取資料,並將結尾標記正規化為 MIME 標準定義的法規 CRLF 標記,最後將結果傳送到標準輸出串流。因此,我們使用檔案來源從標準輸入產生資料,並使用一個正規化資料的篩選器對它進行鏈結。然後泵浦會重複從來源取得資料,並將資料移到檔案匯入,由匯入將資料傳送到標準輸出。
為了讓討論更具體,我們從討論正規化篩選器的實作開始。normalize
程式庫是一個建立這種篩選器的函式。我們的初始篩選器介面如下:篩選器接收輸入資料部分,並傳回已處理資料部分。當不再有輸入資料時,使用者會呼叫 nil
部分來通知篩選器。然後篩選器傳回已處理資料的最後一個部分。
儘管界面極為簡單,但其實作看起來並沒有那麼明顯。符合此界面的任何過濾器都需要在呼叫之間保持某些類型的上下文。這是因為區塊可以在標示行尾的 CR 和 LF 字元之間斷開。這種對內容儲存的需求是促使使用工廠的原因:每次呼叫工廠時,它都會回傳一個具有其自身內容的過濾器,讓我們可以同時使用多個獨立過濾器。對於正規化過濾器,我們明白顯而易見的解決方案(即在產生任何輸出之前將所有輸入串連到內容中)並不好,因此我們必須找到其他方式。
我們將實作分解成兩部分:低層級過濾器和高層級過濾器的工廠。低層級過濾器將以 C 實作,並且在函式呼叫之間不載入任何內容。以 Lua 實作的高層級過濾器工廠將建立並回傳一個高層級過濾器,此過濾器保留低層級過濾器需要的任何內容,但使用它會讓使用者遠離其內部詳細資料。這樣一來,我們便善用 C 的效率執行繁重的工作,並善用 Lua 的簡潔性做為簿記。
以下是高層級行尾正規化過濾器工廠的實作
function filter.cycle(low, ctx, extra) return function(chunk) local ret ret, ctx = low(ctx, chunk, extra) return ret end end function normalize(marker) return cycle(eol, 0, marker) end
normalize
工廠會單純呼叫更通用的工廠,也就是 cycle
工廠。此工廠接收一個低層級過濾器、一個初始內容和一些額外值,並回傳對應的高層級過濾器。每次高層級過濾器使用新的區塊呼叫時,它會呼叫低層級過濾器,並傳遞先前的內容、新的區塊和額外的引數。低層級過濾器會產生經處理資料的區塊和新的內容。最後,高層級過濾器會更新其內部內容,並對使用者回傳經處理的資料區塊。低層級過濾器可以執行所有工作。請注意,此實作善用 Lua 5.0 詞彙範圍規則,在函式呼叫之間當地儲存內容。
轉到低層級過濾器時,我們會注意到行尾標記正規化問題本身並沒有完美的解決方案。困難來自於混合輸入中空行的定義上的固有含糊性。但是,以下解決方案可以很好地用於任何一致的輸入,以及混合輸入中的非空行。它在處理空行時也有相當好的效果,且作為實作低層級過濾器的範例是很好的參考。
我們執行的動作:CR 和 LF 被視為換行候選字元。如果只出現某個候選字元,或後接不同候選字元,我們發出一個行尾行標記。亦即 CR CR 和 LF LF 各發出兩個行尾標記,但 CR LF 和 LF CR 只發出一個標記。此方法能處理 Mac OS、Mac OS X、VMS、Unix、DOS 與 MIME,以及其他較不為人知的慣例。
低階過濾器分成兩個簡單函式。內部函式實際執行轉換。它逐一處理各輸入字元,決定要輸出什麼,以及如何修改內容。內容會說明前一個字元是否為候選字元,若為,則說明是哪個候選字元。
#define candidate(c) (c == CR || c == LF) static int process(int c, int last, const char *marker, luaL_Buffer *buffer) { if (candidate(c)) { if (candidate(last)) { if (c == last) luaL_addstring(buffer, marker); return 0; } else { luaL_addstring(buffer, marker); return c; } } else { luaL_putchar(buffer, c); return 0; } }
內部函式使用 Lua 的輔助函式庫的緩衝介面,以發揮其效率及易用性。外部函式僅與 Lua 互動。它接收內容及輸入區塊(以及一個選用行尾標記),並回傳轉換後的輸出及新內容。
static int eol(lua_State *L) { int ctx = luaL_checkint(L, 1); size_t isize = 0; const char *input = luaL_optlstring(L, 2, NULL, &isize); const char *last = input + isize; const char *marker = luaL_optstring(L, 3, CRLF); luaL_Buffer buffer; luaL_buffinit(L, &buffer); if (!input) { lua_pushnil(L); lua_pushnumber(L, 0); return 2; } while (input < last) ctx = process(*input++, ctx, marker, &buffer); luaL_pushresult(&buffer); lua_pushnumber(L, ctx); return 2; }
請注意,如果輸入區塊為 null
,視為作業已完成。在此情況下,迴圈不會執行一次,且內容重設為初始狀態。這允許過濾器無限重複使用。可能的話,建議撰寫類似的過濾器。
除了上述的行尾標準化過濾器,許多其他過濾器也可以運用相同想法來實作。範例包括 Base64 和 Quoted-Printable 傳輸內容編碼、將文字分成數行、SMTP 位元填充等。最困難的地方在於決定內容為何。例如,對換行來說,內容可能是目前行中剩餘位元組數。對 Base64 編碼來說,內容可能是將輸入分成 3 位元組原子後所剩餘的位元組。
當引入串接概念,過濾器會變得更強大。假設您有一個適用於 Quoted-Printable 編碼的過濾器,且您想要編碼一些文字。根據標準,編碼前必須先將文字標準成標準格式。簡化此任務的絕佳介面是由工廠建立複合過濾器,透過多個過濾器傳遞資料,但可以在任何使用原始濾過器的地方使用。
local function chain2(f1, f2) return function(chunk) local ret = f2(f1(chunk)) if chunk then return ret else return ret .. f2() end end end function filter.chain(...) local f = arg[1] for i = 2, table.getn(arg) do f = chain2(f, arg[i]) end return f end local chain = filter.chain(normalize("\r\n"), encode("quoted-printable")) while 1 do local chunk = io.read(2048) io.write(chain(chunk)) if not chunk then break end end
串接工廠的運作機制非常簡單。它所執行的所有動作就是傳遞資料,讓資料通過所有過濾器,並將資料結果傳回給使用者。它使用較為簡單的輔助函式,該函式知道如何將兩個過濾器串接在一起。在輔助函式中,如果區塊為最終區塊,則必須特別小心。這是因為最終區塊通知必須按序傳遞至兩個過濾器。多虧了串接工廠,很容易就能執行 Quoted-Printable 轉換,如上述範例所示。
正如我們在簡介中所提到的,我們到目前為止所介紹的篩選器在轉換網路中會扮演內部節點的角色。資訊會從一個節點流動到另一個節點(或從一個篩選器流動到下一個),在流動的過程中會被轉換。串連篩選器是我們找到在網路中連接節點的方式。但終端節點呢?在網路的開頭,我們需要一個節點來提供資料,也就是來源。在網路的結尾,我們需要一個節點來接收資料,也就是滙集器。
我們從兩個簡單的來源開始。第一個是 empty
來源:它並不會回傳任何資料,可能會回傳錯誤訊息。第二個是 file
來源,它會逐段產生一個檔案的內容,並在最後關閉檔案處理。
function source.empty(err) return function() return nil, err end end function source.file(handle, io_err) if handle then return function() local chunk = handle:read(2048) if not chunk then handle:close() end return chunk end else return source.empty(io_err or "unable to open file") end end
來源每次被呼叫時都會回傳下一個資料區塊。當沒有更多資料時,它只會回傳 nil
。如果有錯誤,來源可以透過回傳 nil
後,接續回傳錯誤訊息來通知呼叫方。Adrian Sietsma 發現,雖然這並非出於有意,但來源的介面與 Lua 5.0 中的迭代器概念相容。換句話說,資料來源可以很好地與 for
迴圈搭配使用。使用我們的檔案來源作為迭代器,我們可以改寫我們的範例
local process = normalize("\r\n") for chunk in source.file(io.stdin) do io.write(process(chunk)) end io.write(process(nil))
請注意呼叫濾器的最後一次會取得最後一個處理過資料的區塊。當來源回傳 nil
時,迴圈就會終止,因此我們需要在迴圈外進行最後一次呼叫。
來源時常需要在某個事件發生後改變行為。一個簡單的範例是檔案來源希望確保在檔案結束後,不論被呼叫多少次都會回傳 nil
,這可以避免嘗試讀取超過檔案結尾。另一個範例是來源回傳多個檔案的內容,如同它們被串接起來一樣,從一個檔案移動到另一個檔案,直到最後一個檔案的結尾。
實作這種類型的來源方法之一,是讓工廠宣告額外的狀態變數,讓來源透過詞法範圍來使用。我們的檔案來源可以在偵測到檔案結尾時,將檔案處理設為 nil
。然後,每次呼叫來源時,它都能檢查檔案處理是否仍然有效,並據此採取相應的動作
function source.file(handle, io_err) if handle then return function() if not handle then return nil end local chunk = handle:read(2048) if not chunk then handle:close() handle = nil end return chunk end else return source.empty(io_err or "unable to open file") end end
實作此項行為的另一種方式,是針對原始介面進行變更,使其更具彈性。讓我們讓原始程式碼回傳第二個值,除了下一段資料區塊之外。如果回傳的區塊為 nil
,額外的回傳值會告訴我們發生什麼事。第二個 nil
表示沒有更多資料,而且原始程式碼為空。其他任何值都視為錯誤訊息。另一方面,如果區塊不是 nil
,第二個回傳值會告訴我們原始程式碼是否想要被取代。如果為 nil
,我們應繼續使用相同的原始程式碼。否則,其必須為另一個原始程式碼,我們必須從當時起使用該原始程式碼,以取得剩餘的資料。
這個額外的自由對於撰寫原始碼功能的人員來說很好,但是對於必須使用它的人員來說卻很痛苦。很幸運地,只要給予下列其中一種花俏的原始程式碼,我們就能使用下述的工廠,將它轉換為從不需要取代的單純原始程式碼。
function source.simplify(src) return function() local chunk, err_or_new = src() src = err_or_new or src if not chunk then return nil, err_or_new else return chunk end end end
簡化工廠讓我們得以撰寫花俏的原始程式碼,並將其使用,就好像它們是單純的一樣。因此,我們之後的功能只會產生單純的原始程式碼,且使用原始程式碼的功能會預設它們是單純的。
回過頭來看我們的檔案原始程式碼,延伸介面允許一個更優雅的實作。新原始程式碼只要沒有更多資料就會要求改由空原始程式碼取代。不需要重複檢查代碼控制。為了讓使用者更簡便,工廠會在將花俏的檔案原始程式碼回傳給使用者之前讓其簡化。
function source.file(handle, io_err) if handle then return source.simplify(function() local chunk = handle:read(2048) if not chunk then handle:close() return "", source.empty() end return chunk end) else return source.empty(io_err or "unable to open file") end end
如果我們使用 Lua 5.0 的新功能:coroutine,我們可以讓這些概念更強大。coroutine 的宣傳度嚴重不足,而我打算在此扮演我的角色。coroutine 就像詞彙範圍一樣,最初嚐起來很奇怪,但是一旦你習慣這個概念,它可以幫你省下很多時間。我必須承認,使用 coroutine 來實作我們的檔案原始程式碼會殺雞用牛刀,因此讓我們改而實作一個串接式原始程式碼工廠。
function source.cat(...) local co = coroutine.create(function() local i = 1 while i <= table.getn(arg) do local chunk, err = arg[i]() if chunk then coroutine.yield(chunk) elseif err then return nil, err else i = i + 1 end end end) return function() return shift(coroutine.resume(co)) end end
此工廠會建立兩個功能。第一個是以 coroutine 形式提供所有工作輔助程式。它會從其中一個原始程式碼讀取一段資料區塊。如果該區塊為 nil
,它會移至下一個原始程式碼,否則會讓出回傳區塊。當它恢復時,它會從停止的位置繼續,並嘗試讀取下一個區塊。第二個功能是原始程式碼本身,而且僅恢復輔助 coroutine,回傳使用者回傳的任何區塊(跳過第一個結果,那個結果會告訴我們 coroutine 是否已經終止)。想像一下在沒有 coroutine 的情況下撰寫相同的功能,你會發現這個實作相當簡潔。我們會在讓 filter 介面更強大時再次使用 coroutine。
以 filter 串接原始程式碼是什麼意思?最有用的詮釋是,組合的原始程式碼-filter 是一個新的原始程式碼,它會產生資料,並在回傳之前透過 filter 傳遞資料。以下是一個這樣做的工廠
function source.chain(src, f) return source.simplify(function() local chunk, err = src() if not chunk then return f(nil), source.empty(err) else return f(chunk) end end) end
引論中我們的例證結合來源與篩選器。當一個執行緒從來源取得輸入資料時,結合來源與篩選器的概念十分有用。透過串接一個簡單來源與一個或多個篩選器,同一個執行緒可以獲得經過篩選的資料,即使它不知道背後正在進行的篩選作業。
就像我們為資料初階來源定義一個介面,我們也可以為資料最終目的地定義一個介面。我們將所有符合此介面的執行緒稱為接收器。以下為兩個傳回接收器的簡單工廠。資料表工廠會建立一個接收器,用來儲存所有取得的資料到一個資料表中。稍後可以使用 `table.concat` 函式庫執行緒將資料有效率地串接成一個字串。我們再提供 `null` 接收器作為另一個範例:一個接收器簡單地捨棄接收到的資料。
function sink.table(t) t = t or {} local f = function(chunk, err) if chunk then table.insert(t, chunk) end return 1 end return f, t end local function null() return 1 end function sink.null() return null end
接收器接收連續資料段,直到 `nil` 段會通知資料結束。系統會透過 `nil` 段後的參數提供錯誤訊息,通知錯誤發生。如果接收器自行偵測到錯誤,不希望再次被呼叫,它應該傳回 `nil`,也可以再加上一個錯誤訊息。非 `nil` 的傳回值表示來源會接受更多資料。最後,就像來源可以選擇被取代一樣,接收器也可以遵循相同的介面進行取代。話說回來,要實作一個 `sink.simplify` 工廠並不困難,它將複雜的接收器轉換成簡單的接收器。
舉例來說,我們建立一個從標準輸入讀取的來源,然後串接一個用來標準化換行符號約定的篩選器,接著使用接收器將所有資料放入資料表中,最後印出結果。
local load = source.chain(source.file(io.stdin), normalize("\r\n")) local store, t = sink.table() while 1 do local chunk = load() store(chunk) if not chunk then break end end print(table.concat(t))
就像我們建立一個工廠,可以從來源與篩選器產生來源-篩選器鏈結一樣,建立一個工廠,可以從接收器與篩選器產生新接收器也不難。新接收器會先將收到的所有資料傳送給篩選器,處理完畢再傳送至原始接收器。以下是實作方式
function sink.chain(f, snk) return function(chunk, err) local r, e = snk(f(chunk)) if not r then return nil, e end if not chunk then return snk(nil, err) end return 1 end end
在我們的範例中,有一個持續了一段很長的時間的 while 迴圈。這個迴圈始終存在,是因為我們迄今所設計的一切都是被動的。來源、接收器、篩選器:沒有一個能自行執行任何程式碼。將來源可以提供的資料全部抽取並推送至接收器中所執行的操作十分常見,因此我們提供幾個協助函式協助我們執行。
function pump.step(src, snk) local chunk, src_err = src() local ret, snk_err = snk(chunk, src_err) return chunk and ret and not src_err and not snk_err, src_err or snk_err end function pump.all(src, snk, step) step = step or pump.step while true do local ret, err = step(src, snk) if not ret then return not err, err end end end
pump.step
功能將一塊資料從來源移動到儲存區。pump.all
功能帶有一個可選的 step
功能,並使用它來將所有資料從來源泵送至儲存區。我們現在可以使用我們所擁有的所有資料,撰寫一個程式,從磁碟讀取一個二進位檔案,並在將它編碼至 Base64 傳輸內容編碼後將其儲存在另一個檔案中
local load = source.chain( source.file(io.open("input.bin", "rb")), encode("base64") ) local store = sink.chain( wrap(76), sink.file(io.open("output.b64", "w")), ) pump.all(load, store)
我們在此分割過濾器的並不是憑直覺,而是故意的。此外,我們可以鏈結 Base64 編碼過濾器和換行過濾器,然後使用結果過濾器鏈結檔案來源或檔案儲存區。這並不要緊。
事實證明,我們仍然有一個問題。當 David Burgess 編寫 gzip 過濾器時,他注意到解壓縮過濾器可以將小輸入塊爆炸成大量資料。儘管我們希望可以忽略此問題,但我們很快同意無法這麼做。唯一的解決方案是允許過濾器回傳部分結果,而這是我們選擇執行的操作。在呼叫過濾器以傳遞輸入資料後,使用者現在必須迴圈呼叫過濾器以找出它是否有更多輸出資料要回傳。請注意,這些額外的呼叫無法將更多資料傳遞至過濾器。
更具體地說,在將輸入資料塊傳遞至過濾器並收集第一個輸出資料塊後,使用者會重複呼叫過濾器並傳遞空字串,以取得額外的輸出塊。當過濾器本身回傳空字串時,使用者便知道沒有更多輸出資料,並可以繼續傳遞下一個輸入塊。最後,在使用者傳遞 nil
通知過濾器沒有更多輸入資料後,過濾器可能仍產生過多的輸出資料,無法在單一塊中回傳。使用者必須再次迴圈,這次每次傳遞 nil
,直到過濾器本身回傳 nil
以通知使用者它已完全完成。
大部分的過濾器不需要這種額外的權限。幸運的是,新的過濾器介面容易執行。事實上,我們在引言中建立的換行編譯過濾器已經符合它了。另一方面,鏈結功能變得更加複雜。如果不是因為常式,我不會很高興執行它。如果您能找出沒有使用常式的較簡單執行方式,請告訴我!
local function chain2(f1, f2) local co = coroutine.create(function(chunk) while true do local filtered1 = f1(chunk) local filtered2 = f2(filtered1) local done2 = filtered1 and "" while true do if filtered2 == "" or filtered2 == nil then break end coroutine.yield(filtered2) filtered2 = f2(done2) end if filtered1 == "" then chunk = coroutine.yield(filtered1) elseif filtered1 == nil then return nil else chunk = chunk and "" end end end) return function(chunk) local _, res = coroutine.resume(co, chunk) return res end end
鏈結來源也變得更加複雜,但利用常式也可以找到類似的解決方案。鏈結儲存區就像以前一樣簡單。有趣的是,這些修改對不需要新增彈性的過濾器的效能沒有產生任何可測量到的負面影響。它們確實大幅提高了過濾器(例如 gzip 過濾器)的效率,這也是我們保留它們的原因。
這些概念是在開發LuaSocket
[1] 2.0 的過程中創建的,並可用作 LTN12 模組。因此,LuaSocket
[1] 的實作被大幅簡化且變得更加強大。MIME 模組特別整合到 LTN12 中,並提供了許多其他篩選器。我們覺得這些概念值得公諸於世,即使那些不在乎LuaSocket
[1]的人也應如此,因此有了 LTN。
值得一提的額外應用程式會使用身分辨識篩選器。假設您想在檔案下載到接收器時提供一些回饋給使用者。將接收器串連身分辨識篩選器(一個只傳回未經修改的已接收資料的篩選器),您可以在執行過程中更新進度計數器。原始接收器不必修改。另一個有趣的概念是 T 接收器:將資料傳送到其他兩個接收器的接收器。總而言之,似乎有足夠的空間容納許多其他有趣的想法。
在本技術說明中,我們介紹了篩選器,來源,接收器和幫浦。這些通常是資料處理的有用工具。來源提供資料擷取的簡單抽象化。接收器提供最終資料目的地的抽象化。篩選器定義資料轉換的介面。串連篩選器、來源和接收器提供一種優雅的方法,從更簡單的轉換功能建立任意複雜的資料轉換。幫浦只是讓 machinery 開始運作。
在使用 Diego 的以上程式工具時,我發現各種函式雛型的以下簡潔摘要很有用。
function source() -- we have data return chunk -- we have an error return nil, err -- no more data return nil end
function(chunk, src_err) if chunk == nil then -- no more data to process, we won't receive more chunks if src_err then -- source reports an error, TBD what to do with chunk received up to now else -- do something with concatenation of chunks, all went well end return true -- or anything that evaluates to true elseif chunk == "" then -- this is assumed to be without effect on the sink, but may -- not be if something different than raw text is processed -- do nothing and return true to keep filters happy return true -- or anything that evaluates to true else -- chunk has data, process/store it as appropriate return true -- or anything that evaluates to true end -- in case of error return nil, err end
ret, err = pump.step(source, sink) if ret == 1 then -- all ok, continue pumping elseif err then -- an error occured in the sink or source. If in both, the sink -- error is lost. else -- ret == nil and err == nil -- done, nothing left to pump end ret, err = pump.all(source, sink) if ret == 1 then -- all OK, done elseif err then -- an error occured else -- impossible end
function filter(chunk) -- first two cases are to maintain chaining logic that -- support expanding filters (see below) if chunk == nil then return nil elseif chunk == "" then return "" else -- process chunk and return filtered data return data end end
精巧來源的想法是用來讓來源指出從現在起哪一個其他來源包含資料。
function source() -- we have data return chunk -- we have an error return nil, err -- no more data return nil, nil -- no more data in current source, but use sourceB from now on -- ("" could be real data, but should not be nil or false) return "", sourceB end
simple = source.simplify(fancy)
精巧接收器是為了讓接收器指出從現在起哪一個其他接收器處理資料。
function(chunk, src_err) -- same as above (simple sink), except next sinkK sink -- to use indicated after true return true, sinkK end
將精巧接收器轉換為簡單接收器
simple = sink.simplify(fancy)
function filter(chunk) if chunk == nil then -- end of data. If some expanded data is still to be returned return partial_data -- the chains will keep on calling the filter to get all of -- the partial data until the filter return nil return nil elseif chunk == "" then -- a previous filter may have finished returning expanded -- data, now it's our turn to expand the data and return it return partial_data -- the chains will keep on calling the filter to get all of -- the partial data until the filter return "" return "" else -- process chunk and return filtered data, potentially partial. -- In all cases, the filter is called again with "" (see above) return partial_data end end