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