[譯] 語句,訊息和歸約器
語句,訊息和歸約器
在優化程式的設計時,一個通常的建議是將程式拆分成小而獨立的功能單元,以便我們可以隔離元件之間的聯絡,獨立地考慮元件內部的行為。
但是如果這是你優化程式的唯一思路,那麼在實踐中應用它的時候就會有些困難。
在本文中,我將通過一小段程式碼的簡單演進來向你展示如何實踐上述的優化建議,最終我們將達成一個併發程式設計中普遍的模式(在大多數有狀態的程式中都很有用),在此種模式中我們從計算單元的三個不同層面構建我們的程式:“語句”、“訊息” 和 “歸約器”。
你可以在 GitHub 上下載本文的 Swift Playground 。
內容
目標
本文的目的是介紹如何在程式中將狀態獨立起來。有很多我們可能想要這麼做的原因:
- 如果控制邏輯是簡潔的,那麼在單一位置的行為就很容易理解。
- 如果控制邏輯是簡潔的,模式化和理解元件之間的聯絡就很簡單。
- 如果只在單一的位置訪問某個狀態,那麼改變這個訪問入口的執行環境(例如佇列,執行緒,或者一個鎖的內部)將很容易,同樣也可以輕易地將程式變為執行緒安全的或者同步的。
- 如果狀態只能以受限制的方式被訪問,我們就能夠更謹慎地管理依賴,並且在依賴變化時及時更新。
一系列語句
語句是指令式程式設計語言(如 Swift )中的標準計算單元。語句包含賦值,函式和控制流,還可能包括邏輯結果(如狀態變化)。
我知道我是在向程式設計師解釋基本的程式設計術語,我只會簡潔的說明。
下面是一段簡單的程式,其內部的邏輯是由語句組成的:
func printCode(_ code: Int) {
if let scalar = UnicodeScalar(code) {
print(scalar)
} else {
print("�")
}
}
let grinning = 0x1f600
printCode(grinning)
let rollingOnTheFloorLaughing = 0x1f923
printCode(rollingOnTheFloorLaughing)
let notAValidScalar = 0x999999
printCode(notAValidScalar)
let smirkingFace = 0x1f60f
printCode(smirkingFace)
let stuckOutTongueClosedEyes = 0x1f61d
printCode(stuckOutTongueClosedEyes)
這段程式會分行列印如下內容: ? ? � ? ?
上面的被框起來的問號字元不是錯誤,程式碼中故意在將引數轉化為 UnicodeScalar
失敗時列印 Unicode 替代符號(0xfffd
)。
通過訊息控制你的程式
純粹由語句構建的邏輯的最大的問題在於不易於擴充套件。在尋求減少程式碼冗餘的過程中自然地會導致程式碼被資料驅動(至少是部分驅動)。
例如,通過資料驅動上述例子可以將最後的 10 行程式碼減少到 4 行:
let codes = [0x1f600, 0x1f923, 0x999999, 0x1f60f, 0x1f61d]
for c in codes {
printCode(c)
}
當然,上述例子有些過於簡單,可能不能清晰地反映出這種變化。我們可以增加這個例子的複雜性來使差異更加明顯。
我們將陣列中的基本型別 Int
替換成一種需要更多處理的型別。
enum Instruction {
case print
case increment(Int)
case set(Int)
static func array(_ instrs: Instruction...) -> [Instruction] { return instrs }
}
現在,相對於簡單地列印收到的每個 Int
值,我們的處理機需要管理一個內部的 Int
型的儲存器和不同的 Instruction
值,這些 Instruction
值可能會用 .set
方法給儲存器賦值,或者用 .increment
方法給儲存器做累加,又或者用 .print
方法列印儲存器的值。
來看一下我們會用什麼程式碼來處理陣列中的 Instruction
物件:
struct Interpreter {
var state: Int = 0
func printCode() {
if let scalar = UnicodeScalar(state) {
print(scalar)
} else {
print("�")
}
}
mutating func handleInstruction(_ instruction: Instruction) {
switch instruction {
case .print: printCode()
case .increment(let x): state += x
case .set(let x): state = x
}
}
}
var interpreter = Interpreter()
let instructions = Instruction.array(
.set(0x1f600), .print,
.increment(0x323), .print,
.increment(0x999999), .print,
.set(0x1f60f), .print,
.increment(0xe), .print
)
for i in instructions {
interpreter.handleInstruction(i)
}
這段程式碼產生了和之前的例子一樣的輸出,它在內部使用了和之前類似的 printCode
方法,但是實際上是 Interpreter
結構體執行了一小段由 instructions
陣列定義的微程式。
現在可以“更”清楚地看到我們的程式邏輯是由兩個層面上的邏輯組成:
-
handleInstruction
方法和printCode
方法中的 Swift 語句解釋和執行每一條指令。 -
Instructions.array
中包含了一系列需要被解釋的訊息。
我們的第二層計算單元就是所謂的訊息,它可以是任何能夠被放入資料流中傳遞給元件的資料,這些資料流中的資料的結構本身就能夠決定執行結果。
術語提示:我將這些指令稱為“訊息”,這是沿襲了過程演算和參與者模式中的術語用法,但有時候也會使用“命令”這個詞。在某些情況下,這些訊息也會被當成是一種完全的“特定作用域語言”。
通過元件連線構建邏輯
上一節的程式碼最大的問題在於它的結構並不能直觀地反映出計算的結構;我們很難一眼就看出邏輯的走向。
我們需要弄明白計算的結構應該是什麼樣子的。我們做如下嘗試:
- 取一系列的指令
- 將這些指令轉化為一系列對內部狀態的影響
- 將訊息傳遞給能夠實現
列印
動作的第三方控制檯
我們能夠從執行這些任務的 Interpreter
結構體中識別出這幾部分,但是這個結構體沒有被直觀地組織起來以反映出這三個步驟。
所以我們將程式碼重構成能夠直接地展示這種聯絡的樣子。
var state: Int = 0
Instruction.array(
.set(0x1f600), .print,
.increment(0x323), .print,
.increment(0x999999), .print,
.set(0x1f60f), .print,
.increment(0xe), .print
).flatMap { (i: Instruction) -> Int? in
switch i {
case .print: return state
case .increment(let x): state += x; return nil
case .set(let x): state = x; return nil
}
}.forEach { value in
if let scalar = UnicodeScalar(value) {
print(scalar)
} else {
print("�")
}
}
這段程式碼依然會和之前的例子列印同樣的輸出。
現在我們有一個三節的管道,它能夠直接地反映出上面提到的 3 點:一系列指令,解釋指令並對狀態值產生影響,以及輸出階段。
歸約器
我們來看一下管道中間的 flatMap
這一節。為什麼這一節最重要?
不是因為 flatMap
函式本身而是因為我只在這一節中使用了捕獲閉包。 state
變數只在這一節中被捕獲和操作,這相當於 state
的值是 flatMap
閉包的一個私有變數。這個狀態在 flatMap
這一節之外只能被間接地訪問 —— 即只能通過提供一個 Instruction
輸入來設定,同樣也只能通過 flatMap
這一節中選擇傳送的 Int
值來進行訪問。
我們可以將這一節抽象為如下模型:
[圖片上傳失敗...(image-85cfa6-1512576719911)]
作為“歸約器”的管道中某一節的圖表
此圖中每個 a
變數的值都是 Instruction
值。 x
變數的值是 state
, b
變數的值是將被髮送的 Int?
型別的值。
我將之稱為歸約器,這是我想要討論的第三層計算單元。歸約器是一種帶有身份標識( Swift 中的一種引用型別)的實體,其內部狀態只能通過出入的訊息進行訪問。
我說歸約器是我想討論的第三層計算單元是因為我沒有考慮歸約器內部的邏輯,而是把歸約器(典型的 Swift 語句影響被包裝的狀態)當做一個由其和其它單元的連線定義的黑盒單元來考慮,這些黑盒單元是我們設計更高層邏輯的基礎。
另一種解釋是當語句在上下文中執行邏輯時,歸約器通過在執行環境之間跨越形成邏輯。
我使用一個捕獲閉包來將一個 flatMap
函式和一個 Int
變數組成了一個歸約器,但大部分歸約器是類
的例項,這些例項會將它們的狀態維持的更加緊密,並且幫助我們把邏輯整合到更大的邏輯結構中。
用“歸約器”這個詞來描述這種結構來自於程式語言語義學中的歸約語義學。有一個奇怪的術語轉換,“歸約器”也被稱為“累加器”,儘管這兩個詞在語義上近乎對立。這是一個視角的問題:“歸約器”是指將輸入的訊息流歸約成為一個單一的狀態值;而“累加器”則是指在輸入訊息到達時這種結構會將新的資訊累加到它內部的狀態上。
我們還能做些什麼?
我們可以將歸約器的抽象替換為完全不同的機制。
我們可以遷移之前的程式碼,將對 Swift 陣列
值的操作遷移成使用 CwlSignal 響應式程式設計框架,這其中的工作量不只是拖拽操作這麼簡單。這樣做能夠給我們提供非同步能力或者給程式的不同部分提供真實的交流通道。
程式碼如下:
Signal<Instruction>.from(values: [
.set(0x1f600), .print,
.increment(0x323), .print,
.increment(0x999999), .print,
.set(0x1f60f), .print,
.increment(0xe), .print
]).filterMap(initialState: 0) { (state: inout Int, i: Instruction) -> Int? in
switch i {
case .print: return state
case .increment(let x): state += x; return nil
case .set(let x): state = x; return nil
}
}.subscribeValuesAndKeepAlive { value in
if let scalar = UnicodeScalar(value) {
print(scalar)
} else {
print("�")
}
return true
}
這裡的 filterMap
功能更適合作為一個歸約器,因為它提供了真實的內部私有狀態作為 API 的一部分 —— 沒有更多的被捕獲變數需要建立私有狀態 —— 它在語義上等同於之前的 flatMap
,因為它對映了訊號中的一系列值並且過濾掉了可選項。
抽象之間的簡單變化是可實現的,因為歸約器的內容取決於訊息,而不是歸約器機制本身。
除了歸約器之外是否還有其它層次的計算單元?我不清楚,至少我沒遇到過。我們已經解決了狀態封裝的問題,所以任何額外的層次都將是新的問題。但是,如果人工神經網路可以具有“深度學習”,那麼為什麼程式設計不能有“深度語義學”?顯然,這是未來的趨勢 ?。
結論
你可以在 GitHub 上下載本文的 Swift Playground。
這裡的結論是,將程式分解成小而隔離的元件的最自然的方法是以三個不同的層次組織你的程式:
- 歸約器中的狀態程式碼被限制為只有進出的訊息能夠訪問
- 能夠將歸約器執行為指定狀態的訊息
- 歸約器形成的圖表結構組成更高階的程式邏輯
這些都不是什麼新思路;這一切都源自於 20 世紀 70 年代中期的平行計算理論,而且自從 20 世紀 90 年代初“歸約語義學”確立以來,這些思路並沒有大的改變。
當然,這並不意味著人們總是遵循這些好的思路。物件導向程式設計是 20 世紀 90 年代和 21 世紀初人們曾經試圖解決所有程式設計問題的錘子,你可以從物件中構建一個歸約器,但並不意味著所有的物件都是歸約器。物件中沒有限制的介面會使狀態,依賴和介面耦合的維護變得非常困難。
然而,我們可以直接將物件建模為歸約器,只要通過將公共介面簡化成如下內容:
- 構建器
- 接受訊息輸入的方法
- 訂閱或者其它連線到訊息輸出的方法
在這種情況下,限制介面的功能會極大地提供維護和迭代設計的能力。
展望…
在通過元件連線構建邏輯這一節的例子中,我對 flatMap
(不是單子)使用了有爭議的定義。在我的下一篇文章中,我將討論為什麼單子被許多功能程式設計師認為是基本計算單位,而在指令式程式設計中的嚴格實現有時卻並不如非單子的轉換有用。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。
相關文章
- WCF技術剖析之十八:訊息契約(Message Contract)和基於訊息契約的序列化
- 訊息選擇器和方法簽名
- [譯] part 10: switch 語句
- JDBC預編譯語句JDBC編譯
- mysql新增約束語句筆記MySql筆記
- MQTT-保留訊息和遺囑訊息MQQT
- 自定義訊息和對訊息的理解
- dataguard的啟動和應用歸檔日誌的語句
- 深入解析MFC訊息響應和訊息路由路由
- RabbitMQ 和訊息傳遞常用一些術語MQ
- 手把手教你做一個 C 語言編譯器(7):語句編譯
- 訊息粘包 和 訊息不完整 問題
- 【原創】訊息佇列的消費語義和投遞語義佇列
- 源語言、目標語言、翻譯器、編譯器、直譯器編譯
- [譯] part 9: golang 迴圈語句Golang
- SQLite語句(一):表的操作和約束SQLite
- [譯] 關於 PHP 7.4 的最新訊息PHP
- Netty 中的訊息解析和編解碼器Netty
- Java學習之分支結構---判斷語句:if語句和switch語句Java
- Python-條件語句和迴圈語句Python
- Oracle - 約束、索引等相關常用操作語句Oracle索引
- 譯文|如何將 Pulsar 用作訊息佇列佇列
- Linux下邏輯測試語句引數和流程控制語句 if語句Linux
- 理解TON合約中的訊息傳送結構
- iOS 收款推送訊息語音播報iOS
- 如何處理RabbitMQ 訊息堆積和訊息丟失問題MQ
- flask之控制語句 if 語句與for語句Flask
- 給微信伺服器發訊息伺服器
- 英語的靜態句和動態句
- RocketMQ 訊息整合:多型別業務訊息-普通訊息MQ多型型別
- 草根學Python(五) 條件語句和迴圈語句Python
- soar-PHP - SQL 語句優化器和重寫器的 PHP 擴充套件包、 方便框架中 SQL 語句調優PHPSQL優化套件框架
- objc系列譯文(7.4):訊息傳遞機制OBJ
- Go 編譯器內部知識:向 Go 新增新語句-第 2 部分Go編譯
- [譯]如何在Service Worker和網頁客戶端之間傳送訊息網頁客戶端
- 訊息佇列之事務訊息,RocketMQ 和 Kafka 是如何做的?佇列MQKafka
- 用程式碼理解 ObjC 中的傳送訊息和訊息轉發OBJ
- 用程式碼理解ObjC中的傳送訊息和訊息轉發OBJ