新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 牛人業(yè)話 > 編程語言的發(fā)展趨勢及未來方向(3):函數(shù)式編程

編程語言的發(fā)展趨勢及未來方向(3):函數(shù)式編程

作者: 時間:2017-04-05 來源:網(wǎng)絡 收藏

  這是Anders Hejlsberg(不用介紹這是誰了吧)在比利時TechDays 2010所做的開場演講。由于最近我在博客上關于語言的討論比較多,出于應景,也打算將Anders的演講完整地聽寫出來。在上一部分中,Anders闡述了他眼中聲明式編程的理念及DSL,并演示C#中一種內(nèi)部DSL的形式:LINQ。在這一部分中,Anders談及了聲明式編程的另一個重要組成部分:函數(shù)式編程,并使用.NET平臺上的函數(shù)式F#進行了演示。

本文引用地址:http://butianyuan.cn/article/201704/346198.htm

  如果沒有特別說明,所有的文字都直接翻譯自Anders的演講,并使用我自己的口語習慣表達出來,對于Anders的口誤及反復等情況,必要時在譯文中自然也會進行忽略。為了方便理解,我也會將視頻中關鍵部分進行截圖,而某些代碼演示則會直接作為文章內(nèi)容發(fā)表。

  (聽寫開始,接上篇)

    

 

  關于聲明式編程的還有一部分重要的內(nèi)容,那便是函數(shù)式編程。函數(shù)式編程已經(jīng)有很長時間的歷史了,當年LISP便是個函數(shù)式。除了LISP以外我們還有其他許多函數(shù)式,如APL、Haskell、Scheme、ML等等。關于函數(shù)式編程在學術界已經(jīng)有過許多研究了,在大約5到10年前許多人開始吸收和整理這些研究內(nèi)容,想要把它們?nèi)谌敫鼮橥ㄓ玫木幊陶Z言?,F(xiàn)在的編程語言,如C#、、Ruby、Scala等等,它們都受到了函數(shù)式編程語言的影響。

    

 

  我想在這里先花幾分鐘時間簡單介紹一下我眼中的函數(shù)式編程語言。我發(fā)現(xiàn)很多人聽說過函數(shù)式編程語言,但還不十分清楚它們和普通的命令式編程語言究竟有什么區(qū)別。如今我們在使用命令式編程語言寫程序時,我們經(jīng)常會寫這樣的語句,嗨,x等于x加一,此時我們大量依賴的是狀態(tài),可變的狀態(tài),或者說變量,它們的值可以隨程序運行而改變。

  可變狀態(tài)非常強大,但隨之而來的便是叫做“副作用”的問題。在使用可變狀態(tài)時,你的程序則會包含副作用,比如你會寫一個無需參數(shù)的void方法,然后它會根據(jù)你的調(diào)用次數(shù)或是在哪個線程上進行調(diào)用對程序產(chǎn)生影響,因為void方法會改變程序內(nèi)部的狀態(tài),從而影響之后的運行效果。

  而在函數(shù)式編程中則不會出現(xiàn)這個情況,因為所有的狀態(tài)都是不可變的。你可以聲明一個狀態(tài),但是不能改變這個狀態(tài)。而且由于你無法改變它,所以在函數(shù)式編程中不需要變量。事實上對函數(shù)式編程的討論更像是數(shù)學、公式,而不像是程序語句。如果你把x = x + 1這句話交給一個程序員看,他會說“啊,你在增加x的值”,而如果你把它交給一個數(shù)學家看,他會說“嗯,我知道這不是true”。

    

 

  然而,如果你給他看這條語言,他會說“啊,y等于x加一,就是把x + 1的計算結(jié)果交給y,你是為這個計算指定了一個名字”。這時候在思考時就是另一種方式了,這里y不是一個變量,它只是x + 1的名稱,它不會改變,永遠代表了x + 1。

  所以在函數(shù)式編程語言中,當你寫了一個函數(shù),接受一些參數(shù),那么當你調(diào)用這個函數(shù)時,影響函數(shù)調(diào)用的只是你傳進去的參數(shù),而你得到的也只是計算結(jié)果。在一個純函數(shù)式編程語言中,函數(shù)在計算時不會對進行一些神奇的改變,它只會使用你給它的參數(shù),然后返回結(jié)果。在函數(shù)式編程語言中,一個void方法是沒有意義的,它唯一的作用只是讓你的CPU發(fā)熱,而不能給你任何東西,也不會有副作用。當然現(xiàn)在你可能會說,這個CPU發(fā)多少熱也是一個副作用,好吧,不過我們現(xiàn)在先不討論這個問題。

    

 

  這里的關鍵在于,你解決問題的方法和以前大不一樣了。我這里還是用代碼來說明問題。使用函數(shù)式語言寫沒有副作用的代碼,就好比在Java或C#中使用final或是readonly的成員。

  例如這里,我們有一個Point類,構(gòu)造函數(shù)接受x和y,還有一個MoveBy方法,可以把一個點移動一些位置。 在傳統(tǒng)的命令式編程中,我們會改變Point實例的狀態(tài),這么做在平時可能不會有什么問題。但是,如果我把一個Point對象同時交給3個API使用,然后我修改了Point,那么如何才能告訴它們狀態(tài)改變了呢?可能我們可以使用事件,blablabla,如果我們沒有事件,那么就會出現(xiàn)那些不愉快的副作用了。

  那么使用函數(shù)式編程的形式寫代碼,你的Point類還是可以包含狀態(tài),例如x和y,不過它們是readonly的,一旦初始化以后就不能改變了。MoveBy方法不能改變Point對象,它只能創(chuàng)建一個新的Point對象并返回出來。這就是一個創(chuàng)建新Point對象的函數(shù),不是嗎?這樣就可以讓調(diào)用者來決定是使用新的還是舊的Point對象,但這里不會有產(chǎn)生副作用的情況出現(xiàn)。

  在函數(shù)式編程里自然不會只有Point對象,例如我們會有集合,如Dictionary,Map,List等等,它們都是不可變的。在函數(shù)式編程中,當我們向一個List里添加元素時,我們會得到一個新的List,它包含了新增的元素,但之前的List依然存在。所以這些數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)方式是有根本性區(qū)別的,它們的內(nèi)部結(jié)構(gòu)會設法讓這類操作變的盡可能高效。

  在函數(shù)式編程中訪問狀態(tài)是十分安全的,因為狀態(tài)不會改變,我可以把一個Point或List對象交給任意多的地方去訪問,完全不用擔心副作用。函數(shù)式編程的十分容易并行,因為我在運行時不會修改狀態(tài),因此無論多少線程在運行時都可以觀察到正確的狀態(tài)。兩個函數(shù)完全無關,因此它們是并行還是順序地執(zhí)行便沒有什么區(qū)別了。我們還可以有延遲計算,可以進行Memorization,這些都是函數(shù)式編程中十分有趣的方面。

  你可能會說,那么我們?yōu)槭裁床欢加眠@種方法來寫程序呢?嗯,最終,就像我之前說的那樣,我們不能只讓CPU發(fā)熱,我們必須要把計算結(jié)果表現(xiàn)出來。那么我們在屏幕上打印內(nèi)容時,或者把數(shù)據(jù)寫入文件或是Socket時,其實就產(chǎn)生了副作用。因此真實世界中的函數(shù)式編程,往往都是把純粹的部分進行隔離,或是進行更細致的控制。事實上也不會有真正純粹的函數(shù)式編程語言,它們都會帶來一定的副作用或是命令式編程的能力。但是,它們默認是函數(shù)式的,例如在函數(shù)式編程語言中,所有東西默認都是不可變的,你必須做些額外的事情才能使用可變狀態(tài)或是產(chǎn)生危險的副作用。此時你的編程觀念便會有所不同了。

    

 

  我們在自己的環(huán)境中開發(fā)出了這樣一個函數(shù)式編程語言,F(xiàn)#,已經(jīng)包含在VS 2010中了。F#誕生于微軟劍橋研究院,由Don Syme提出,他在F#上已經(jīng)工作了5到10年了。F#使用了另一個函數(shù)式編程語言OCaml的常見核心部分,因此它是一個強類型語言,并支持一些如模式匹配,類型推斷等現(xiàn)代函數(shù)式編程語言的特性。在此之上,F(xiàn)#又增加了異步工作流,度量單位等較為前沿的語言功能。

  而F#最為重要的一點可能是,在我看來,它是第一個和工業(yè)級的框架和工具集,如.NET和Visual Studio,有深入集成的函數(shù)式編程語言。F#允許你使用整個.NET框架,它和C#也有類似的執(zhí)行期特征,例如強類型,而且都會生成高效的代碼等等。我想,現(xiàn)在應該是展示一些F#代碼的時候了。

    

 

  首先我想先從F#中我最喜歡的特性講起,這是個F#命令行……(打開命令行窗口以及一個F#源文件)……F#包含了一個交互式的命令行,這允許你直接輸入代碼并執(zhí)行。例如輸入5……x等于5……然后x……顯示出x的值是5。然后讓sqr x等于x乘以x,于是我這里定義了一個簡單的函數(shù),名為sqr。于是我們就可以計算sqr 5等于25,sqr 10等于100。

  F#的使用方式十分動態(tài),但事實上它是一個強類型的編程語言。我們再來看看這里。這里我定義了一個計算平方和的函數(shù)sumSquares,它會遍歷每個列表中每個元素,平方后再把它們相加。讓我先用命令式的方式編寫這個函數(shù),再使用函數(shù)式的方式,這樣你可以看出其中的區(qū)別。

  let sumSquaresI l =

  let mutable acc = 0

  for x in l do

  acc <- acc + sqr x

  acc

  這里先是命令式的代碼,我們先創(chuàng)建一個累加器acc為0,然后遍歷列表l,把平方加到acc中,然后最后我返回acc。有幾件事情值得注意,首先為了創(chuàng)建一個可變的狀態(tài),我必須顯式地使用mutable進行聲明,在默認情況下這是不可變的。

    

 

  還有一點,這段代碼里我沒有提供任何的類型信息。當我把鼠標停留在方法上時,就會顯示sumSquaresI方法接受一個int序列作為參數(shù)并返回一個int。你可能會想int是哪里來的,嗯,它是由類型推斷而來的。編譯器從這里的0發(fā)現(xiàn)acc必須是一個int,于是它發(fā)現(xiàn)這里的加號表示兩個int的相加,于是sqr函數(shù)返回的是個int,再接下來blablabla……最終它發(fā)現(xiàn)這里到處都是int。

    

 

  如果我把這里修改為浮點數(shù)0.0,鼠標再停留一下,你就會發(fā)現(xiàn)這個函數(shù)接受和返回的類型都變成float了。所以這里的類型推斷功能十分強大,也十分方便。

    

 

  現(xiàn)在我可以選擇這個函數(shù),讓它在命令行里執(zhí)行,然后調(diào)用sumSquaresI,提供1到100的序列,就能得到結(jié)果了。

  let rec sumSquaresF l =

  match l with

  | [] -> 0

  | h :: t -> sqr h + sumSquaresF t

  那么現(xiàn)在我們來換一種函數(shù)式的風格。這里是另一種寫法,可以說是純函數(shù)式的實現(xiàn)方式。如果你去理解這段代碼,你會發(fā)現(xiàn)有不少數(shù)學的感覺。這里我定義了sumSqauresF函數(shù),輸入一個l列表,然后使用下面的模式去匹配l。如果它為空,則結(jié)果為0,否則把列表匹配為頭部和尾部,然后便將頭部的平方和尾部的平方和相加。

  你會發(fā)現(xiàn),在計算時我不會去改變?nèi)魏我粋€變量的值,我只是創(chuàng)建新的值。我這里會使用遞歸,就像在數(shù)學里我們經(jīng)常使用遞歸,把一個公式分解成幾個變化的形式,以此進行遞歸的定義。在編程時我們也使用遞歸的做法,然后編譯器會設法幫我們轉(zhuǎn)化成尾遞歸或是循環(huán)等等。

  于是我們便可以執(zhí)行sumSquaresF函數(shù),也可以得到相同的結(jié)果。當然實際上可能你并不會像之前這樣寫代碼,你可能會使用高階函數(shù):

  let sumSquares l = Seq.sum (Seq.map (fun x -> x * x) l )

  例如這里,我只是把函數(shù)x乘以x映射到列表上,然后相加。這樣也可以得到相同的結(jié)果,而且這可能是更典型的做法。我這里只是想說明,這個語言在編程時可能會給你帶來完全不同的感受,雖然它的執(zhí)行期特征和C#比較接近。

  這便是關于F#的內(nèi)容。

  (未完待續(xù))

 



關鍵詞: 編程語言 Python

評論


相關推薦

技術專區(qū)

關閉