編程語言的發(fā)展趨勢(shì)及未來方向(3):函數(shù)式編程
這是Anders Hejlsberg(不用介紹這是誰了吧)在比利時(shí)TechDays 2010所做的開場(chǎng)演講。由于最近我在博客上關(guān)于語言的討論比較多,出于應(yīng)景,也打算將Anders的演講完整地聽寫出來。在上一部分中,Anders闡述了他眼中聲明式編程的理念及DSL,并演示C#中一種內(nèi)部DSL的形式:LINQ。在這一部分中,Anders談及了聲明式編程的另一個(gè)重要組成部分:函數(shù)式編程,并使用.NET平臺(tái)上的函數(shù)式編程語言F#進(jìn)行了演示。
本文引用地址:http://m.butianyuan.cn/article/201704/346198.htm如果沒有特別說明,所有的文字都直接翻譯自Anders的演講,并使用我自己的口語習(xí)慣表達(dá)出來,對(duì)于Anders的口誤及反復(fù)等情況,必要時(shí)在譯文中自然也會(huì)進(jìn)行忽略。為了方便理解,我也會(huì)將視頻中關(guān)鍵部分進(jìn)行截圖,而某些代碼演示則會(huì)直接作為文章內(nèi)容發(fā)表。
(聽寫開始,接上篇)
關(guān)于聲明式編程的還有一部分重要的內(nèi)容,那便是函數(shù)式編程。函數(shù)式編程已經(jīng)有很長(zhǎng)時(shí)間的歷史了,當(dāng)年LISP便是個(gè)函數(shù)式編程語言。除了LISP以外我們還有其他許多函數(shù)式編程語言,如APL、Haskell、Scheme、ML等等。關(guān)于函數(shù)式編程在學(xué)術(shù)界已經(jīng)有過許多研究了,在大約5到10年前許多人開始吸收和整理這些研究?jī)?nèi)容,想要把它們?nèi)谌敫鼮橥ㄓ玫木幊陶Z言?,F(xiàn)在的編程語言,如C#、Python、Ruby、Scala等等,它們都受到了函數(shù)式編程語言的影響。
我想在這里先花幾分鐘時(shí)間簡(jiǎn)單介紹一下我眼中的函數(shù)式編程語言。我發(fā)現(xiàn)很多人聽說過函數(shù)式編程語言,但還不十分清楚它們和普通的命令式編程語言究竟有什么區(qū)別。如今我們?cè)谑褂妹钍骄幊陶Z言寫程序時(shí),我們經(jīng)常會(huì)寫這樣的語句,嗨,x等于x加一,此時(shí)我們大量依賴的是狀態(tài),可變的狀態(tài),或者說變量,它們的值可以隨程序運(yùn)行而改變。
可變狀態(tài)非常強(qiáng)大,但隨之而來的便是叫做“副作用”的問題。在使用可變狀態(tài)時(shí),你的程序則會(huì)包含副作用,比如你會(huì)寫一個(gè)無需參數(shù)的void方法,然后它會(huì)根據(jù)你的調(diào)用次數(shù)或是在哪個(gè)線程上進(jìn)行調(diào)用對(duì)程序產(chǎn)生影響,因?yàn)関oid方法會(huì)改變程序內(nèi)部的狀態(tài),從而影響之后的運(yùn)行效果。
而在函數(shù)式編程中則不會(huì)出現(xiàn)這個(gè)情況,因?yàn)樗械臓顟B(tài)都是不可變的。你可以聲明一個(gè)狀態(tài),但是不能改變這個(gè)狀態(tài)。而且由于你無法改變它,所以在函數(shù)式編程中不需要變量。事實(shí)上對(duì)函數(shù)式編程的討論更像是數(shù)學(xué)、公式,而不像是程序語句。如果你把x = x + 1這句話交給一個(gè)程序員看,他會(huì)說“啊,你在增加x的值”,而如果你把它交給一個(gè)數(shù)學(xué)家看,他會(huì)說“嗯,我知道這不是true”。
然而,如果你給他看這條語言,他會(huì)說“啊,y等于x加一,就是把x + 1的計(jì)算結(jié)果交給y,你是為這個(gè)計(jì)算指定了一個(gè)名字”。這時(shí)候在思考時(shí)就是另一種方式了,這里y不是一個(gè)變量,它只是x + 1的名稱,它不會(huì)改變,永遠(yuǎn)代表了x + 1。
所以在函數(shù)式編程語言中,當(dāng)你寫了一個(gè)函數(shù),接受一些參數(shù),那么當(dāng)你調(diào)用這個(gè)函數(shù)時(shí),影響函數(shù)調(diào)用的只是你傳進(jìn)去的參數(shù),而你得到的也只是計(jì)算結(jié)果。在一個(gè)純函數(shù)式編程語言中,函數(shù)在計(jì)算時(shí)不會(huì)對(duì)進(jìn)行一些神奇的改變,它只會(huì)使用你給它的參數(shù),然后返回結(jié)果。在函數(shù)式編程語言中,一個(gè)void方法是沒有意義的,它唯一的作用只是讓你的CPU發(fā)熱,而不能給你任何東西,也不會(huì)有副作用。當(dāng)然現(xiàn)在你可能會(huì)說,這個(gè)CPU發(fā)多少熱也是一個(gè)副作用,好吧,不過我們現(xiàn)在先不討論這個(gè)問題。
這里的關(guān)鍵在于,你解決問題的方法和以前大不一樣了。我這里還是用代碼來說明問題。使用函數(shù)式語言寫沒有副作用的代碼,就好比在Java或C#中使用final或是readonly的成員。
例如這里,我們有一個(gè)Point類,構(gòu)造函數(shù)接受x和y,還有一個(gè)MoveBy方法,可以把一個(gè)點(diǎn)移動(dòng)一些位置。 在傳統(tǒng)的命令式編程中,我們會(huì)改變Point實(shí)例的狀態(tài),這么做在平時(shí)可能不會(huì)有什么問題。但是,如果我把一個(gè)Point對(duì)象同時(shí)交給3個(gè)API使用,然后我修改了Point,那么如何才能告訴它們狀態(tài)改變了呢?可能我們可以使用事件,blablabla,如果我們沒有事件,那么就會(huì)出現(xiàn)那些不愉快的副作用了。
那么使用函數(shù)式編程的形式寫代碼,你的Point類還是可以包含狀態(tài),例如x和y,不過它們是readonly的,一旦初始化以后就不能改變了。MoveBy方法不能改變Point對(duì)象,它只能創(chuàng)建一個(gè)新的Point對(duì)象并返回出來。這就是一個(gè)創(chuàng)建新Point對(duì)象的函數(shù),不是嗎?這樣就可以讓調(diào)用者來決定是使用新的還是舊的Point對(duì)象,但這里不會(huì)有產(chǎn)生副作用的情況出現(xiàn)。
在函數(shù)式編程里自然不會(huì)只有Point對(duì)象,例如我們會(huì)有集合,如Dictionary,Map,List等等,它們都是不可變的。在函數(shù)式編程中,當(dāng)我們向一個(gè)List里添加元素時(shí),我們會(huì)得到一個(gè)新的List,它包含了新增的元素,但之前的List依然存在。所以這些數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)方式是有根本性區(qū)別的,它們的內(nèi)部結(jié)構(gòu)會(huì)設(shè)法讓這類操作變的盡可能高效。
在函數(shù)式編程中訪問狀態(tài)是十分安全的,因?yàn)闋顟B(tài)不會(huì)改變,我可以把一個(gè)Point或List對(duì)象交給任意多的地方去訪問,完全不用擔(dān)心副作用。函數(shù)式編程的十分容易并行,因?yàn)槲以谶\(yùn)行時(shí)不會(huì)修改狀態(tài),因此無論多少線程在運(yùn)行時(shí)都可以觀察到正確的狀態(tài)。兩個(gè)函數(shù)完全無關(guān),因此它們是并行還是順序地執(zhí)行便沒有什么區(qū)別了。我們還可以有延遲計(jì)算,可以進(jìn)行Memorization,這些都是函數(shù)式編程中十分有趣的方面。
你可能會(huì)說,那么我們?yōu)槭裁床欢加眠@種方法來寫程序呢?嗯,最終,就像我之前說的那樣,我們不能只讓CPU發(fā)熱,我們必須要把計(jì)算結(jié)果表現(xiàn)出來。那么我們?cè)谄聊簧洗蛴?nèi)容時(shí),或者把數(shù)據(jù)寫入文件或是Socket時(shí),其實(shí)就產(chǎn)生了副作用。因此真實(shí)世界中的函數(shù)式編程,往往都是把純粹的部分進(jìn)行隔離,或是進(jìn)行更細(xì)致的控制。事實(shí)上也不會(huì)有真正純粹的函數(shù)式編程語言,它們都會(huì)帶來一定的副作用或是命令式編程的能力。但是,它們默認(rèn)是函數(shù)式的,例如在函數(shù)式編程語言中,所有東西默認(rèn)都是不可變的,你必須做些額外的事情才能使用可變狀態(tài)或是產(chǎn)生危險(xiǎn)的副作用。此時(shí)你的編程觀念便會(huì)有所不同了。
我們?cè)谧约旱沫h(huán)境中開發(fā)出了這樣一個(gè)函數(shù)式編程語言,F(xiàn)#,已經(jīng)包含在VS 2010中了。F#誕生于微軟劍橋研究院,由Don Syme提出,他在F#上已經(jīng)工作了5到10年了。F#使用了另一個(gè)函數(shù)式編程語言O(shè)Caml的常見核心部分,因此它是一個(gè)強(qiáng)類型語言,并支持一些如模式匹配,類型推斷等現(xiàn)代函數(shù)式編程語言的特性。在此之上,F(xiàn)#又增加了異步工作流,度量單位等較為前沿的語言功能。
而F#最為重要的一點(diǎn)可能是,在我看來,它是第一個(gè)和工業(yè)級(jí)的框架和工具集,如.NET和Visual Studio,有深入集成的函數(shù)式編程語言。F#允許你使用整個(gè).NET框架,它和C#也有類似的執(zhí)行期特征,例如強(qiáng)類型,而且都會(huì)生成高效的代碼等等。我想,現(xiàn)在應(yīng)該是展示一些F#代碼的時(shí)候了。
首先我想先從F#中我最喜歡的特性講起,這是個(gè)F#命令行……(打開命令行窗口以及一個(gè)F#源文件)……F#包含了一個(gè)交互式的命令行,這允許你直接輸入代碼并執(zhí)行。例如輸入5……x等于5……然后x……顯示出x的值是5。然后讓sqr x等于x乘以x,于是我這里定義了一個(gè)簡(jiǎn)單的函數(shù),名為sqr。于是我們就可以計(jì)算sqr 5等于25,sqr 10等于100。
F#的使用方式十分動(dòng)態(tài),但事實(shí)上它是一個(gè)強(qiáng)類型的編程語言。我們?cè)賮砜纯催@里。這里我定義了一個(gè)計(jì)算平方和的函數(shù)sumSquares,它會(huì)遍歷每個(gè)列表中每個(gè)元素,平方后再把它們相加。讓我先用命令式的方式編寫這個(gè)函數(shù),再使用函數(shù)式的方式,這樣你可以看出其中的區(qū)別。
let sumSquaresI l =
let mutable acc = 0
for x in l do
acc <- acc + sqr x
acc
這里先是命令式的代碼,我們先創(chuàng)建一個(gè)累加器acc為0,然后遍歷列表l,把平方加到acc中,然后最后我返回acc。有幾件事情值得注意,首先為了創(chuàng)建一個(gè)可變的狀態(tài),我必須顯式地使用mutable進(jìn)行聲明,在默認(rèn)情況下這是不可變的。
還有一點(diǎn),這段代碼里我沒有提供任何的類型信息。當(dāng)我把鼠標(biāo)停留在方法上時(shí),就會(huì)顯示sumSquaresI方法接受一個(gè)int序列作為參數(shù)并返回一個(gè)int。你可能會(huì)想int是哪里來的,嗯,它是由類型推斷而來的。編譯器從這里的0發(fā)現(xiàn)acc必須是一個(gè)int,于是它發(fā)現(xiàn)這里的加號(hào)表示兩個(gè)int的相加,于是sqr函數(shù)返回的是個(gè)int,再接下來blablabla……最終它發(fā)現(xiàn)這里到處都是int。
如果我把這里修改為浮點(diǎn)數(shù)0.0,鼠標(biāo)再停留一下,你就會(huì)發(fā)現(xiàn)這個(gè)函數(shù)接受和返回的類型都變成float了。所以這里的類型推斷功能十分強(qiáng)大,也十分方便。
現(xiàn)在我可以選擇這個(gè)函數(shù),讓它在命令行里執(zhí)行,然后調(diào)用sumSquaresI,提供1到100的序列,就能得到結(jié)果了。
let rec sumSquaresF l =
match l with
| [] -> 0
| h :: t -> sqr h + sumSquaresF t
那么現(xiàn)在我們來?yè)Q一種函數(shù)式的風(fēng)格。這里是另一種寫法,可以說是純函數(shù)式的實(shí)現(xiàn)方式。如果你去理解這段代碼,你會(huì)發(fā)現(xiàn)有不少數(shù)學(xué)的感覺。這里我定義了sumSqauresF函數(shù),輸入一個(gè)l列表,然后使用下面的模式去匹配l。如果它為空,則結(jié)果為0,否則把列表匹配為頭部和尾部,然后便將頭部的平方和尾部的平方和相加。
你會(huì)發(fā)現(xiàn),在計(jì)算時(shí)我不會(huì)去改變?nèi)魏我粋€(gè)變量的值,我只是創(chuàng)建新的值。我這里會(huì)使用遞歸,就像在數(shù)學(xué)里我們經(jīng)常使用遞歸,把一個(gè)公式分解成幾個(gè)變化的形式,以此進(jìn)行遞歸的定義。在編程時(shí)我們也使用遞歸的做法,然后編譯器會(huì)設(shè)法幫我們轉(zhuǎn)化成尾遞歸或是循環(huán)等等。
于是我們便可以執(zhí)行sumSquaresF函數(shù),也可以得到相同的結(jié)果。當(dāng)然實(shí)際上可能你并不會(huì)像之前這樣寫代碼,你可能會(huì)使用高階函數(shù):
let sumSquares l = Seq.sum (Seq.map (fun x -> x * x) l )
例如這里,我只是把函數(shù)x乘以x映射到列表上,然后相加。這樣也可以得到相同的結(jié)果,而且這可能是更典型的做法。我這里只是想說明,這個(gè)語言在編程時(shí)可能會(huì)給你帶來完全不同的感受,雖然它的執(zhí)行期特征和C#比較接近。
這便是關(guān)于F#的內(nèi)容。
(未完待續(xù))
評(píng)論