摘要:在和的參數列表里,順序不重要,這是并發本質對于只請求一個的情況,和沒有區別。在并發編程里之需要并發組合這一種操作符,不需要再發明一個順序組合操作符號,因為它只是并發組合的一個特例。
花了很久的時間學習π calculus;天資愚鈍至今尚未學明白,好在不影響寫代碼。
任何一種和計算或者編程相關的數學理論,都可以有兩種不同的出發點:一種是可以作為基礎理論(或計算模型)解釋程序員們每天用各種語言寫下的代碼,它背后的本質是怎樣的;這就象用物理學解釋生活里看到的各種自然現象;另一種是通過對理論的學習,了解到它在概念層面上具體解決了什么問題,以及針對哪類問題特別有效,在編程開發實踐中嘗試應用其思想。
后一種相對玄學,但是反過來說這個思考和實踐的過程對理解理論很有幫助。
π和λ一樣很抽象,離編程實踐很遠,而且,完全存在可能性,一個完整的實踐需要語言和運行環境一級支持。但是學習一件事物呢,不要太功利,找到樂趣開動思維是最重要的,在能真正在工程上大面積應用之前,不妨就把它看作是一個益智游戲。這樣的心態就會讓學習變得富有樂趣,不容易焦慮或者有挫折感。
我不打算從符號入手講解π,但是它的基礎概念要交代一下。
π是關于進程的算術(或者叫演算);算術(Calculus)一詞不如想象的那么嚇人,不要因為曾經噩夢般的考試生活對它天生恐懼。算術的意思只是說,我們希望我們的代碼里的構件,類也好,方法也好,他們是可以如此靈活的組合使用的,就像我們在數學上的運算符,可以算整數、自然數、復數、向量、矩陣、張量、等等;數學上有很多的運算符可用,大多數運算符都能應用在相當廣泛的數學對象上;所以我們說數學系統是豐富的,是強大的思維工具和解決問題的方法。
說π是進程算術的意思很自然,就是構建一個系統時把它看作是很多進程的組合;在這里進程的含義和我們在代碼中寫下的函數差不多,但是它不是指操作系統意義上的進程,也不像λ那樣可以描述純函數。
除了過程,π里只有一個概念:通訊。構成系統的多個進程,包括大量實際系統中的動態過程,他們用通訊的方式交互;這兩者就構成了系統的全部。
π里的通訊和Golang或者CSP里的channel,或者,Alan Kay定義的那種OO或者Actor Model里的message,又或者,我們實際在編程中使用的socket或者ipc,有沒有關系?關系肯定是有的,但是π里定義的通訊比所有這些都更加純粹;而且,在π里只有通訊這一件事;這預示著,在這個系統里的所有行為,都由通訊來完成。
我們來看一下π里最基礎也是最重要的一個表達式:
|
(這個表達式在segmentfault的顯示有誤,應該是一行,中間用vertical pipe,在π里表示并發組合)
在|左側的表達式的意思是,有一個叫做c的通訊通道,可以收到一個值,收到這個值之后P才可以開始演算(估值),P里面的x,都替換成收到的值;當然這個值是個常數是我們最喜聞樂見的,但實際上也可能收到一個完整的π表達式(就成了High Order了)。
在右側的表達式和左側相反,它指的是P過程如果要開始演算,前提條件是向通訊通道c發送一個y出去;這個從程序員的角度看感覺可能沒法理解,console.log()之后才能繼續執行是什么意思?好像從來沒有遇到過輸出阻塞程序運行而且讓程序員傷腦筋的事兒。
但是這個表達式在π里很重要;在編程里同樣很重要。
輸出前綴在π里表述的意思是一個過程被blocking到有請求時才開始。比如實現一個readable stream,在buffer里的數據枯竭或者低于警戒線的時候才會啟動代碼讀取更多數據填充buffer。
而前面這個表達式,可以看作是沒有buffer的兩個過程,一個讀,一個寫;然后兩側的過程都可以開始執行,而且,是以并發的方式。在π里,或者其他類似的符號系統里,這種表達式變換叫做reduction,和數學表達式銷項簡化是一樣的。
所以我們寫下的第一個玩具級代碼片段里,這個類的名字就叫做Reducer。
Reducer可以接受一個callback形式的函數作為生產者(producer),producer等待到reducer對象的on方法被調用時開始執行,當它產生結果時更新reducer對象的error或者data成員,同時,等待這個值的函數(在調用on時被保存在consumers成員數組中,被全部調用。
這個producer只能運行一次,如果完成之后還有on請求,會同步調用請求函數。只工作一次這個限制讓這個類無法做到可變更數據的觀察,不過那不是我們現在需要考慮的問題。
class Reducer { constructor (producer) { if (typeof producer !== "function") throw new Error("producer must be a function") this.producer = producer } on (f) { if (Object.prototype.hasOwnProperty.call(this, "data") || Object.prototype.hasOwnProperty.call(this, "error")) { f() } else { if (this.consumers) { this.consumers.push(f) } else { this.consumers = [f] this.producer((err, data) => { if (err) { this.error = err } else { this.data = data } const consumers = this.consumers delete this.producer delete this.consumers consumers.forEach(f => f()) }) } } } }
那么你可能會問,node.js里有emitter了,還有各種stream,為什么要多帶帶寫這樣一個Reducer?
在成品的開發框架中提供的類,一般都是完善的工具,它包含的不只有一個概念,而且要應對很多實際的使用需求。
而我們這里更強調概念,這是第一個原因;第二個原因,是reducer更原始(primitive),它不是用于繼承的,也沒有定義任何事件名稱,即,它沒有行為語義。
node.js里的emitter可以在π的意義上看作一個表達式,每一個類似write之類的方法都是一個通訊channel,每一個on的事件名稱也是一個通訊channel,換句話說,它不是一個基礎表達式。
把一個非基礎表達式作為一個基礎構件是設計問題,當我們需要表達它沒有提供的更基礎或者更靈活的語義要求時就有麻煩,比如我們有兩個event source其中一個出錯時:
const src1onData = data => { ... } const src1onError = err => { src1.removeListener("data", src1onData) src1.removeListener("error", src1onError) src1.on("error", () => {}) // mute further error src2.removeListener("data", src2onData) src2.removeListener("error", src2onError) src2.on("error", () => {}) // mute further error src1.destroy() src2.destroy() callback(err) } const src2onData = data => { ... } const src2onError = err => { .... } source1.on("data", src1onData) source1.on("error", src1onError) source2.on("data", src2onData) source2.on("error", src1onError)
在node.js里類似這樣的代碼不在少數;造成這個困難的原因,就是“互斥”這個在π里只要一個加號(+)表示的操作,在emitter里受到了限制;而且emitter的代碼已經有點重了,自己重載不是很容易。
在看實際使用代碼之前來看一點小小的算術邏輯。
// one finished const some = (...rs) => { let next = rs.pop() let fired = false let f = x => !fired && (fired = true, next(x)) rs.forEach(r => r.on(f)) } // all finished const every = (...rs) => { let next = rs.pop() let arr = rs.map(r => undefined) let count = rs.length rs.forEach((r, i) => r.on(x => (arr[i] = x, (!--count) && next(...arr)))) } module.exports = { reducer: f => new Reducer(f), some, every, }
就像javascript的數組方法一樣,我們希望能夠靈活表達針對一組reducer的操作。比如第一個some方法;它用了javascript的rest parameters特性,參數中最后一個是函數,其他的都是reducer,這樣使用代碼的形式最好讀。
some的意思是同時on多個reducer,但只要有一個有值了,最后一個參數函數就被調用。
every的意思也是同時on多個reducer,但需要全部有值,才會繼續。
這里的代碼很原始,而且對資源不友好,但用于說明概念可以了。
最后來看一點實際使用的代碼:
// bluetooth addr (from ssh) const baddr = reducer(callback => getBlueAddr(ip, callback)) // bluetooth device info const binfo = reducer(callback => pi.every(baddr, () => ble.deviceInfo(baddr.data, callback)))
第一個reducer是baddr是取設備藍牙地址的;getBlueAddr是很簡單還是很復雜沒關系。這句話說明讀取baddr在當前上下文下沒有其他依賴性,可以直接執行;但是這個語句并沒有立刻開始讀取藍牙地址的過程。它相當于我們前面寫的π表達式:
即過程P(getBlueAddr)能產生(輸出)一個藍牙地址,但是它會一直等到有人來讀的時候才會開始運行。
出發這個過程開始執行的代碼在在最后一句,在binfo的producer里。這個pi.every(...)的調用,就相當于:
因為這個代碼在binfo的producer里,所以它還沒開始執行,也不會和baddr的producer發生reduction。
在binfo的producer代碼里出現了對另一個reducer的on, pi.every, pi.some之類的操作,就直接表述了binfo對baddr的依賴關系。這是這種看起來有點小題大作的寫法的一個好處,就是你閱讀代碼時依賴性是一目了然。
這兩行代碼在運行后,兩個producer過程都沒開始,因為沒有一個reducer被on了。如果你需要觸發這個過程,可以寫:
pi.every(binfo, () => { console.log("test every", baddr.data, binfo.data) })
當然這個寫法在并發編程里不推薦,因為你是讀了binfo的代碼知道依賴性的,否則console.log可能會發生錯誤。推薦的做法是一股腦把你要的reducer都寫到every或some里去,他們之間的依賴性對every或者some的回調函數來說是黑盒的:
pi.every(baddr, binfo, () => { console.log("test every", baddr.data, binfo.data) })
無論是some還是every,都是讓所有被請求的reducer的producers同時開始工作,即并發組合。在every和some的參數列表里,順序不重要,這是并發本質;對于只請求一個reducer的情況,every和some沒有區別。
如果你需要順序組合,大概可以這樣寫:
pi.every(baddr, () => pi.every(binfo, () => { ... }))
不過為什么會需要順序呢?我們在寫流程代碼的時候需要的,不是順序,是依賴性;偶爾發生的完全沒有數據傳遞的順序,比如另一個讀取文件的過程必須等到一個寫入文件的過程結束,也可以理解為前面一個過程產生了一個null結果是后面們一個過程需要的。
上面這句話是Robin Milner在他的圖靈獎獲獎發言里說的。在并發編程里之需要并發組合這一種操作符,不需要再發明一個順序組合操作符號,因為它只是并發組合的一個特例。
在node.js里,因為異步特性,分號(;)是語言意義上的順序組合,但是模型意義上的并發組合。callback, emitter, promise,async/await,以及上面的這個形同柯里化的pi.every語句,都是順序組合的表達。但是我相信你看完這篇文章后會理解,在并發編程里,只有局部是為了便于書寫需要這種順序組合。并發編程和順序編程的本質不同,是前者在表達依賴性,而不是順序。
我鼓勵你用Reducer寫點實際的代碼,雖然它不能應對連續變化的值,只是單發(one-shot)操作,但很多時候也是可以的,比如寫流程,或者寫http請求。
而說道寫流程,我不得不說π的一大神奇特性,就是它的通訊語義已經足夠表達所有流程。就像你在這里看到的代碼一樣,事實上用π可以構件整個程序表達順序。
事實上我在最近幾周就在寫測試代碼。有大量的set up/tear down和各種通訊。不同的測試配置。用π寫出來的代碼我最終不關心每個測試下如何做不同的初始化,因為代碼全部是Lazy的,我只要在最后用every一次性Pull所有我要的reducer即可。
至于執行順序,老實說我也不曉得。這就是并發編程!
這里有一點rx的味道對嗎?
不過我不熟悉rx,我需要的也不是數據流模型;我關注的是過程的組合,如何清晰的看出依賴性,如何優雅的處理錯誤。
這里寫的Reducer非常有潛力,它體現在:
你看到了every和some,實際上我們可以做很多復雜的邏輯在里面,比如第一個錯誤,比如錯誤類型的過濾器,比如收集夠指定數量的結果就返回;
分開錯誤處理和成功的代碼路徑是可能的,Reducer里可以只on錯誤結果,或者正確結果;
而最重要的rx的不同,是reducer里可以裝入比簡單的callback更rich的函數或者對象,例如有cancel方法的,能emit progress事件的,等等;
前面說過,π里有一個+號表示互斥過程;象some或者every一樣寫一個互斥的on多個reducer,很容易;
互斥的一個較為復雜的情況是conditional的,這個其實也很容易寫,相當于reducer級聯了,寫在前面的用于條件估值;更復雜的情況的是pattern matching,即用pattern選擇繼續執行的過程,那就更帥了,用庫克的話說,I am thrilled;
All in all,還是那句老話,less is more。Emitter的設計錯誤在于它的目的是提供繼承,而不是用于實現靈活的代數方法。
當然,reducer也只是剛剛開始。幾個月后,我會再回來的。
補:文中所述的最基礎的π的reduction的嚴格表述如下,左側的name z從channel x出去后被vertical pipe右側接收到,Q表達式里的y因此全部替換成z,[z/y]用于表述這個替換,稱為alpha-conversion,而這個表達式從左側到右側的變換,就是beta-reduction。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106944.html
摘要:過程是一個計算單元,計算是通過通訊來完成的。標題的表達式里還有一個符號,表示一個無行為的過程。一個過程的是它和外部產生行為交互的唯一方式。所以如果兩個過程需要通過一個交互,這個必須在兩個過程中都是,其中一方用于發送,另一方用于接收。 這篇文章的標題是一個π表達式,結尾是一段JavaScript代碼,和這個表達式的含義完全一致,或者說,完成了這個表達式的估值。 π演算(π calculu...
摘要:算法中的馬氏鏈轉移以上采樣過程中,如圖所示,馬氏鏈的轉移只是輪換的沿著坐標軸軸和軸做轉移,于是得到樣本馬氏鏈收斂后,最終得到的樣本就是的樣本,而收斂之前的階段稱為。 作者:chen_h微信號 & QQ:862251340微信公眾號:coderpai簡書地址:https://www.jianshu.com/p/278... 本文是整理網上的幾篇博客和論文所得出來的,所有的原文連接都在文...
摘要:有不對的地方,或者有更好的理解,請告訴我,謝謝原理以容器的中心點作為圓心,以高和寬的最小值作為直徑畫圓,將圓以,,,,,,,劃分為四個象限,鼠標進入容器時的點的值在這四個象限里分別對應容器邊框的下,右,上,左。 $(#wrap).bind(mouseenter mouseleave,function(e) { var w = $(this).width(); var h...
昔者莊周夢為胡蝶,栩栩然胡蝶也。自喻適志與!不知周也。俄然覺,則蘧蘧然周也。不知周之夢為胡蝶與?胡蝶之夢為周與? ——典出《莊子·齊物論》 其故事大意為:莊周夢見自己變成一只蝴蝶,栩栩如生,感到十分愉快和愜意!不知道自己原本是莊周。突然間醒過來,驚惶不定之間方知原來自己是莊周。不知道是莊周夢中變成蝴蝶呢,還是蝴蝶夢見自己變成莊周呢? 莊周夢蝶是一則非常浪漫的寓言故事,它揭示了一個道理:這個紛繁...
閱讀 1769·2021-10-19 13:30
閱讀 1335·2021-10-14 09:48
閱讀 1531·2021-09-22 15:17
閱讀 2007·2019-08-30 15:52
閱讀 3273·2019-08-30 11:23
閱讀 1983·2019-08-29 15:27
閱讀 887·2019-08-29 13:55
閱讀 753·2019-08-26 14:05