昨天看一些函數(shù)式編程、Haskell、Scala的書和文章,到半夜還莫名其妙:
「好像除了函數(shù)是一等公民外,沒(méi)發(fā)現(xiàn)什么好處呀……很多人說(shuō)學(xué)函數(shù)式能改變思維方式,又不肯說(shuō)這改變具體是什么。」
「javascript里函數(shù)也是一等公民,用著也很爽,函數(shù)式比之,還有什么好處?」
很不滿意地躺下,用手機(jī)繼續(xù)google之,突然發(fā)現(xiàn)一篇文章,醍醐灌頂,立刻爬起來(lái)仔細(xì)閱讀,摸黑記錄(興奮得沒(méi)工夫開燈),發(fā)微博致謝。
「函數(shù)式編程的關(guān)鍵是不定義變量」,這至為重要的一點(diǎn),之前看的書和文章竟然都沒(méi)強(qiáng)調(diào)——說(shuō)是說(shuō)過(guò)了,卻沒(méi)指出這是重點(diǎn)。
就連忙現(xiàn)想個(gè)功能,嘗試不定義變量地實(shí)現(xiàn)它,發(fā)現(xiàn)當(dāng)有了這個(gè)勒定,函數(shù)語(yǔ)言自然會(huì)被「倒逼」著具備lambda等特性,因?yàn)槲野l(fā)現(xiàn)只用老java,不用java8語(yǔ)法,很難在「不定義變量」的約束下實(shí)現(xiàn)之,不知道是不是不可能實(shí)現(xiàn),我的數(shù)學(xué)能力不足以證明之。
今天早上起來(lái),趕快翻開《寫給大忙人看的java8》,使用java8語(yǔ)法完成程序。完成后重構(gòu)一下,發(fā)現(xiàn)果然不出所料,最終代碼的結(jié)構(gòu),是函數(shù)復(fù)函數(shù),函數(shù)套函數(shù),主函數(shù)是超級(jí)大函數(shù),而這些函數(shù)的分隔和關(guān)聯(lián),著眼點(diǎn)就在其參數(shù)和返回值上。
數(shù)學(xué)里,函數(shù)的意義是定義域到值域的映射,代碼里寫的函數(shù),其意義是參數(shù)集合到返回值集合的映射,為了達(dá)成這從集合到集合的映射,在不許定義變量的情況下,只有借助java8的lambda和stream才能實(shí)現(xiàn)。
每個(gè)函數(shù)都是從一個(gè)集合,變換到另一個(gè)集合,這樣經(jīng)過(guò)若干步變換,就實(shí)現(xiàn)了從程序的總參數(shù),到程序的總結(jié)果的變換。
程序的意義本來(lái)就是從輸入到輸出的變換,輸入是用戶操作、網(wǎng)絡(luò)請(qǐng)求、定時(shí)器等事件引起的中斷,及存儲(chǔ)器某個(gè)地方現(xiàn)有的數(shù)據(jù),輸出則是將一些數(shù)據(jù)寫到存儲(chǔ)器中,包括寫磁盤、寫顯存以讓屏幕顯示某些圖畫、寫數(shù)據(jù)庫(kù)、發(fā)網(wǎng)絡(luò)請(qǐng)求調(diào)遠(yuǎn)處接口等。可以把程序的「總輸入?yún)?shù)」看做一個(gè)集合,「總輸出結(jié)果」看做一個(gè)集合,從參數(shù)集合到結(jié)果集合的映射,就是一個(gè)「大函數(shù)」,這與函數(shù)式編程的「主函數(shù)是超級(jí)大函數(shù)」是一致的。
其實(shí)「網(wǎng)站」的「主函數(shù)」又何嘗不是「超級(jí)大函數(shù)」呢,本質(zhì)就是socket服務(wù)端的主循環(huán),通過(guò)很復(fù)雜的策略,把「總輸入」包成request對(duì)象,把「總輸出」準(zhǔn)備用response對(duì)象處置,然后路由到具體的程序上,那程序可能進(jìn)一步「分治」,用框架把request的參數(shù)綁到具體處理方法的入?yún)⑸希言摲椒ǖ某鰠⒂每蚣芙o到response返回,然后通過(guò)路徑匹配,把各個(gè)請(qǐng)求給到適當(dāng)?shù)奶幚矸椒ㄉ稀V皇牵芏鄷r(shí)候,我們?cè)谔邔印⑻M窄的上下文中工作得太久,以至于忘了自己身處一個(gè)「超級(jí)大函數(shù)」之中。
視線更大一點(diǎn),計(jì)算機(jī)又何嘗不是「超級(jí)大函數(shù)」呢,一通電,CPU即不停運(yùn)轉(zhuǎn),這就是最大的函數(shù),「總輸入」是電能,「總輸出」是電路使硬件發(fā)生的變化;在這之上一層,操作系統(tǒng)的主循環(huán)是次一級(jí)的「超級(jí)大函數(shù)」,「總輸入」是隨時(shí)監(jiān)聽到的硬件中斷;再之上一層,才是某個(gè)進(jìn)程的主循環(huán)這「大函數(shù)」,「總輸入」是操作系統(tǒng)分配來(lái)的事件,上段說(shuō)的網(wǎng)站服務(wù)器的socket主循環(huán)就是這種「大函數(shù)」;在這函數(shù)調(diào)用之下,F(xiàn)ilter和Servlet這些組件的生命周期才得以運(yùn)轉(zhuǎn),然后進(jìn)一步才是Spring等框架的初始化,Struts或Spring等框架表現(xiàn)層組件的初始化和響應(yīng)事件方法執(zhí)行,再下一步,才是業(yè)務(wù)層,持久層,讀寫磁盤,讀寫數(shù)據(jù)庫(kù),讀寫API這些具體的「小函數(shù)」,小函數(shù)的輸出,再經(jīng)過(guò)層層包裹,成為「超級(jí)大函數(shù)」的輸出的一部分,即對(duì)某處的硬件產(chǎn)生效果。
視線再大一點(diǎn),就是宇宙大爆炸是「究極大函數(shù)」,那個(gè)瞬間一切原子的位置,是否已決定了之后的一切,這已是哲學(xué)的問(wèn)題了。這個(gè)「洪荒之力」,過(guò)于究極,我們也只是這個(gè)大函數(shù)中極為渺小的一些數(shù)據(jù)啊,緣聚,就有了我,緣散,就沒(méi)了我,像一滴水消失在水中,像計(jì)算機(jī)斷電后,內(nèi)存中的010101化為烏有,人生天地間,忽如遠(yuǎn)行客。
說(shuō)遠(yuǎn)了。函數(shù)想從輸入變換到輸出,處理方法是當(dāng)然要有的,這個(gè)方法,命令式語(yǔ)言著眼于「參數(shù)怎樣算出結(jié)果」的流程,而函數(shù)式語(yǔ)言著眼于「參數(shù)與結(jié)果的對(duì)應(yīng)」的關(guān)系,一個(gè)著眼于過(guò)程,一個(gè)著眼于結(jié)果,一個(gè)在厚重的書本中尋找來(lái)龍去脈,一個(gè)用搜索引擎來(lái)直指目的訊息,一個(gè)運(yùn)籌帷幄,一個(gè)應(yīng)變隨機(jī),一個(gè)深厚,一個(gè)扁平,一個(gè)動(dòng),一個(gè)靜,一個(gè)重步驟,一個(gè)重設(shè)置,若說(shuō)沒(méi)奇緣,java偏又升到8,若說(shuō)有奇緣,為何陣營(yíng)終分化。
映射這件事,java程序員也早就熟悉啊,不論是web.xml,還是struts.xml,還是spring中bean的id到實(shí)體,還是mybatis中「函數(shù)」的id到語(yǔ)句,hibernate?它叫做O-R什么來(lái)著?服務(wù)器這「超級(jí)大函數(shù)」接收到的各路神仙,給分化瓦解,分而治之,你雖一路來(lái),我卻N路去,這是但凡程序員都知道的「分治」原則。順序選擇循環(huán),順序循環(huán)可以咬牙不要,不用選擇還想達(dá)到目的的,沒(méi)聽說(shuō)過(guò),即使沒(méi)聽過(guò)「函數(shù)式」的程序員,也每天都在做把「大函數(shù)」的任務(wù)視情況分配到「小函數(shù)」來(lái)處理的工作。
那「不定義變量」的意義是什么,它的意義是,函數(shù)是純邏輯——當(dāng)然,是大邏輯套小邏輯的復(fù)雜邏輯。它不需要「知識(shí)」,即存儲(chǔ)在存儲(chǔ)單元中的數(shù)據(jù)。就好比形式邏輯,A為真,B為真,A且B則一定為真,至于A和B是什么,不用想。「不定義變量」從根本上避免了來(lái)自知識(shí)的污染,對(duì)程序來(lái)說(shuō),參數(shù)即是全部知識(shí),外部知識(shí)不需要,我也沒(méi)興趣向外寫,我只以這點(diǎn)有限的參數(shù)知識(shí)為準(zhǔn),返回適當(dāng)?shù)慕Y(jié)果知識(shí)。
這個(gè)切割非常漂亮,能讓程序員專注于對(duì)局部的考察,只關(guān)注當(dāng)前函數(shù)這「茅廬」即可,外面洪水滔天都不在意(畢竟它們滲不進(jìn)來(lái))。顯然,這閱讀代碼、單元測(cè)試起來(lái),都太方便了,有限知識(shí)的純邏輯推演,正是數(shù)學(xué)和邏輯學(xué)「天然真理」在程序世界中的表現(xiàn),天然真理是干凈的,不需要被證明,不可能被證偽。
但科學(xué)是「有待」的,商業(yè)程序也是「有待」的,商業(yè)程序里可能沒(méi)有用全局變量裝狀態(tài),但最大的狀態(tài)都在用N個(gè)數(shù)據(jù)庫(kù)服務(wù)器或緩存服務(wù)器的集群裝著呢。數(shù)據(jù)就是知識(shí),知識(shí)就是力量,阿爾法打敗了李世石,在這真正的洪荒之力面前,純邏輯的推演顯得力不從心,互聯(lián)網(wǎng)時(shí)代,記憶力和計(jì)算力都外包,計(jì)算力這方面函數(shù)式干凈利落,但記憶力即「狀態(tài)」的價(jià)值同樣舉足輕重啊。科學(xué)知識(shí),早晚要被證偽,并非天理,但能指導(dǎo)生活;商業(yè)程序,其中含有狀態(tài),并不干凈,但能幫助生活。函數(shù)式的思維,應(yīng)該作為一種武器,納入我們的兵器譜,在適當(dāng)?shù)臅r(shí)候取出來(lái),破軍殺敵。而不是奉為至寶,頂禮膜拜,一言不合,出言不遜。這才是一個(gè)客觀務(wù)實(shí)的態(tài)度。
以上是初識(shí)函數(shù)式編程的一點(diǎn)漫談,沒(méi)有限于問(wèn)題本身,「漫溢」了,不過(guò)作為散文,而非論文,「通感移覺」這種「使用知識(shí)」的修辭,很有效用,但雖然形散,神卻不散,不知看到這里(感謝)的你,對(duì)這會(huì)點(diǎn)頭否?霧里看花,見識(shí)有限,全是個(gè)人思考和領(lǐng)悟的產(chǎn)物,恐怕錯(cuò)誤不少,請(qǐng)多指正。
最后把開頭說(shuō)的用java嘗試函數(shù)式編程的代碼貼上來(lái)吧,java8是剛學(xué),代碼挺笨的,請(qǐng)指教。
public static void main(String[] args) {
//把給定的一些字符串,統(tǒng)計(jì)字母出現(xiàn)次數(shù),結(jié)果為:
//a:1 b:4 k:3 o:6
System.out.println(strs2SumGroupStr("booka", "bookb", "koob"));
}
//booka,bookb... -> 'a:1 b:4..'
public static String strs2SumGroupStr(String... strs){
return stream2SumGroupStr(strs2Stream(strs));
}
//booka,bookb... -> b,o,o,k,a,b...
public static IntStream strs2Stream(String... strs){
return String.join("", strs).chars();
}
//b,o,o,k,a,b... -> 'a:1 b:4..'
public static String stream2SumGroupStr(IntStream is){
return groupMap2SumGroupStr(stream2GroupMap(is));
}
//b,o,o,k,a,b... -> {a:*,b:*..}
public static Map stream2GroupMap(IntStream is){
return is.boxed().collect(Collectors.groupingBy(i->i, Collectors.summarizingInt(i->i)));
}
//{a:*,b:*..} -> 'a:1 b:4..'
public static String groupMap2SumGroupStr(Map map){
return groupSet2SumGroupStr(groupMap2GroupSet(map));
}
//{a:*,b:*..} -> ('a:1','b:4'..)
public static Set groupMap2GroupSet(Map map){
return map
.keySet()
.stream()
.map(k->(char)k.intValue()+":"+map.get(k).getCount())
.collect(Collectors.toSet());
}
//('a:1','b:4'..) -> 'a:1 b:4..'
public static String groupSet2SumGroupStr(Set set){
return set
.stream()
.reduce((x,y) -> x + " " + y)
.get();
}