函數(shù)式編程心得
最近一年來筛欢,我在函數(shù)式編程上不停探索浸锨,想要構(gòu)建一個屬于自己的編程思維。然而在實際開發(fā)過程中版姑,經(jīng)常會因為各種原因?qū)е滤悸坊靵y柱搜,難以繼續(xù)開發(fā)。
所以這篇文章主要總結(jié)一下自己的開發(fā)思維模式剥险,以便自己開發(fā)過程中能有經(jīng)驗所依聪蘸。
當需要開發(fā)一個系統(tǒng)時,需要明確一件事情表制,即任何計算機程序健爬,都可以理解為解釋器。
eval和apply的矛盾統(tǒng)一
既然涉及到解釋器么介,那么免不了談?wù)揺val和apply兩個函數(shù)娜遵。
eval函數(shù)作用是求值表達式,apply函數(shù)是使用指定參數(shù)去調(diào)用某個函數(shù)壤短。如下所示
(eval '(+ 1 1)')
(apply + (list 1 1))
任何程序都是這兩個函數(shù)的循環(huán)遞歸調(diào)用设拟。就如SICP里面的求值器慨仿,eval求值一個表達式的時候,如果該表達式是程序纳胧,就將函數(shù)名和參數(shù)傳遞個apply镰吆,求出結(jié)果并返回。如下所示:
(define (eval exp env)
(cond ...
[(application? exp)
(apply (eval (operator exp) env)
(list-of-values (operands exp) env))]
[else
(error 'eval "Unknow expression type -- EVAL" exp)]))
(define (apply procedure arguments)
(cond [(primitive-procedure? procedure)
(apply-primitive-procedure procedure arguments)]
[(compound-procedure? procedure)
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure)))]
[else
(error 'apply "Unknow procedure type -- APPLY" procedure)]))
eval需要調(diào)用apply躲雅,而apply也需要調(diào)用eval來求值子表達式。這就稱為元循環(huán)骡和。
而eval和apply從本質(zhì)上看是同一個東西相赁,它們都是在指定環(huán)境下對表達式進行求值,看看下面它們的入?yún)ⅲ?/p>
(define (eval expr env))
(define (apply proc args))
對于eval來說慰于,它的表達式類型很多钮科,每個表達式都必須在環(huán)境下對其求值。而apply需要函數(shù)作為參數(shù)婆赠,其實這作為參數(shù)的函數(shù)也可以被認為是表達式绵脯,而該作為參數(shù)的函數(shù)所需的信息,也是可以被認為是環(huán)境休里。不同的地方僅僅在于eval的環(huán)境比較單一蛆挫,而apply則豐富多樣。對應(yīng)地妙黍,eval的表達式非常豐富悴侵,而apply的表達式單一。
所以它們兩個都是解釋器拭嫁,都是在特定環(huán)境下對表達式進行解釋可免。
計算機程序是解釋器
理解了這一個層面,就能快速理解復(fù)雜系統(tǒng)背后的精髓做粤。比如浇借,大家熟悉的MYSQL看似復(fù)雜精深,其實它對我們而言只是一個解釋器怕品,它解釋我們編寫的SQL語言妇垢,并把解釋結(jié)果返回給我們。這個解釋器的環(huán)境包含著我們所說的表肉康,數(shù)據(jù)修己,索引等等。而對SQL解釋的過程迎罗,就是不斷的對環(huán)境中的各種數(shù)據(jù)進行查詢和修改睬愤。
又比如,我們?nèi)粘K鶎懙腤eb系統(tǒng)纹安,也不過是另一種解釋器而已尤辱。該解釋器的表達式就是用戶發(fā)送過來的http請求砂豌,而求值環(huán)境就是數(shù)據(jù)庫,緩存等等光督。對表達式的求值程序阳距,被我們拆分成路由,控制器结借,具體業(yè)務(wù)邏輯等等層面而已筐摘。
把所有程序當作是解釋器,可以擴寬我們的編程思維船老。而一旦我們想要運用這個編程思維去設(shè)計程序的時候咖熟,我們就必須深刻理解解釋器背后設(shè)計思維和概念。
比如柳畔,分析一個模塊時馍管,我們可以思考該模塊到底是什么樣的解釋器,這個解釋器的表達式有哪些薪韩,分別有哪些作用确沸,解釋器的求值環(huán)境到底是什么?
舉個具體例子俘陷,現(xiàn)在需要做一個http服務(wù)器罗捎,其功能是從socket中讀取用戶發(fā)送的字節(jié)流,并轉(zhuǎn)換成json格式拉盾。
按照過去的思維去思考宛逗,功能無非就是寫一個函數(shù),其入?yún)⑹莝ocket字節(jié)流盾剩,返回是json格式雷激。然后就開始編寫這個函數(shù)的邏輯:不斷的讀取input的內(nèi)容,對這些內(nèi)容進行解析告私,而后把解析內(nèi)容組合起來屎暇,形成json格式,并返回驻粟。
用解釋器的設(shè)計思維根悼,同樣需要寫這個函數(shù),只不過這個函數(shù)的實現(xiàn)方式更加細致靈活蜀撑。這種實現(xiàn)方式挤巡,不再簡單地把socket字節(jié)流當成是參數(shù),而是把字節(jié)流當做是求值環(huán)境酷麦,而我們需要做的是構(gòu)建一些合理的表達式矿卑,在特定環(huán)境下,當用自己編寫的解釋器去求值表達式沃饶,從而將socket字節(jié)流轉(zhuǎn)換成json格式母廷。
表達式設(shè)計思維
運用解釋器思維來設(shè)計系統(tǒng)的過程中轻黑,第一步要做的事情,也是最難的事情就是設(shè)計表達式琴昆,設(shè)計良好的表達式能夠讓上層程序員高效開發(fā)應(yīng)用氓鄙。這往往有三個重要問題需要思考:基本表達式是什么,如何組合基本表達式业舍,如何構(gòu)建它們的抽象等抖拦。
下面,我將試著闡述這三個問題舷暮。
首先要明確基本表達式的通用定義态罪,我給它定義為用戶操作環(huán)境的基本方式,它屏蔽環(huán)境的細節(jié)脚牍,使得用戶可以更流程表達想法向臀。換句話說只要是能夠操作環(huán)境巢墅,并且能讓用戶無需關(guān)注環(huán)境的诸狭,都算是基本表達式。
然而基礎(chǔ)表達式的功能是很單一的君纫,我們需要對基礎(chǔ)表達式進行各種形式的組合驯遇,才能完整的表達一件事物。因此蓄髓,如何對基本元素進行組合叉庐,是一件很必要的事情。組合形式一般而已無關(guān)緊要会喝,最關(guān)鍵的在于組合表達式的意義陡叠。有些組合表達式,將順序求值子表達式肢执,并將子表達式的結(jié)果匯總起來枉阵,有些組合表達式卻是組成一條調(diào)用鏈,后表達式依賴前表達式的結(jié)果预茄。
抽象應(yīng)該如何理解呢兴溜?舉例來說,掀開被子耻陕,坐立起來拙徽,穿好鞋子,這些動作我們可以匯總起來叫做起床诗宣,而起床就是這些動作的抽象膘怕。只要我對自己說起床,我的身體就做那些動作召庞,就叫做使用抽象淳蔼。在構(gòu)建表達式的時候侧蘸,我們?yōu)榱烁鲿车乇磉_自己的想法,同樣需要一些方式進行抽象和使用抽象鹉梨。
舉例說讳癌,define
, +
, lambda
, 數(shù)字,變量都是scheme的基本表達式存皂,這些基礎(chǔ)表達式的意義都依賴環(huán)境晌坤,甚至define
表達式還會改變環(huán)境,然而基礎(chǔ)表達式很好屏蔽了環(huán)境旦袋,使得用戶無需了解環(huán)境的任何細節(jié)骤菠。
(define a (lambda () (+ 1 1) (+ 2 2)))
這是一種組合方式,即通過list將基本表達式組合起來疤孕,形成功能更加強大的表達式商乎。
(define a (lambda () (+ 1 1) (+ 2 2)))
這是一種組合方式,即通過list將基本表達式組合起來祭阀,形成功能更加強大的表達式鹉戚。
而lambda表達式是一種抽象方式,它將多個表達式組裝形成一個匿名函數(shù)专控。調(diào)用該匿名函數(shù)抹凳,即相當于調(diào)用lambda體里的組合表達式。由于define過程把 a
與一個lambda關(guān)聯(lián)起來伦腐,也就是說 a
可以說是lambda的抽象赢底,只要調(diào)用a,就相當于調(diào)用lambda里的過程柏蘑。換句話說幸冻,抽象是通過lambda
來封裝,define
建立關(guān)聯(lián)咳焚。
由于我們要描述一些表達式洽损,可以采用下面這種數(shù)學標準方法來描述需要構(gòu)建的表達式。
此外黔攒,使用編譯器思維去設(shè)計程序趁啸,是要達成最終目的的,因此也需要以目標導(dǎo)向原則來設(shè)計表達式督惰。例如要將字節(jié)流轉(zhuǎn)換為json格式不傅。轉(zhuǎn)換過程中,需要從字節(jié)流轉(zhuǎn)換成字符串赏胚,再提取里面的內(nèi)容访娶,組合成json。因此觉阅,這里涉及到轉(zhuǎn)換表達式崖疤,提取表達式秘车,組合表達式。
解釋器設(shè)計心得
我沒有寫過復(fù)雜的編譯器劫哼,所以在設(shè)計編譯器的方面有一定欠缺叮趴,這里總結(jié)出來的心得或許不夠成熟。
表達式可以采用面向?qū)ο笏季S進行封裝权烧,這是做模塊化最基本的單元眯亦。而在scheme中,對象是可以使用算子來模擬的般码,而且算子還具備其他語言外更靈活的特性妻率。
一個復(fù)雜的編譯器,往往由多個子編譯器組合而成板祝。不要試圖用一個編譯器去囊括所有功能宫静,而是應(yīng)該拆分成職責明確的子編譯器。對于功能單一的模塊券时,能用簡單函數(shù)取代就不要用編譯器理念來復(fù)雜化模塊孤里。
復(fù)雜編譯器有多個子編譯器組成,而子編譯器也有更細小更具體的編譯器所組成革为。我們是否能將把編譯器也看成是表達式扭粱,子編譯器的排列組合舵鳞,就像一個復(fù)合表達式包含多個子表達式震檩。因此,是否可以試圖夠構(gòu)造一個更高層次的編譯器蜓堕,讓編譯器去生成編譯器抛虏?