天天看點

函數式程式設計

昨天看RxJava時提到了函數式程式設計:

函數式程式設計

今天在看極客時也遇到了講解:

雖然C語言簡單靈活,能夠讓程式員在進階語言特性之上輕松進行底層上的微觀控制,被譽為 進階語言中的彙編語言 ,

但其基于過程和底層的設計初衷又成了它的短闆.

在程式世界中,程式設計工作更多的是解決業務上的問題,而不是計算機的問題,我們需要更為貼近業務,更為抽象的語言,如典型的面向對象語言C++和Java等.

C++很大程度上解決了C語言中的各種問題和不便,尤其是通過類,模闆,虛函數和運作時識别等解決了C語言的泛型程式設計問題.

然而,如何做更為抽象的泛型呢?答案就是函數式程式設計(Functional Programming)

相對于計算機的曆史而言,函數式程式設計其實是一個非常古老的概念.函數式程式設計的基礎模型來源于λ演算,而λ演算并沒有被設計在計算機上執行.

我們來看一下函數式程式設計,它的理念就來自于數學中的代數

假設f(x)是一個函數,g(x)是第二個函數,把f(x)這個函數套下來,并展開.然後還可以定義一個由兩個一進制函數組合成的二進制函數,還可以做遞歸,下面這個函數定義就是斐波那契數列

對于函數式程式設計來說,它隻關心定義輸入資料和輸出資料相關的關系,數學表達式裡面其實是在做一種映射(mapping),

輸入的資料和輸出的資料關系是什麼樣的,是用函數來定義的.

函數式程式設計有以下特點:

特征

stateless: 函數不維護任何狀态.函數式程式設計的核心精神是stateless,簡而言之就是它不能存在狀态,打個比方,你給我資料我處理完扔出來.裡面的資料是不變的.

immutable: 輸入資料是不能動的,動了輸入資料就有危險,是以要傳回新的資料集.

優勢:

沒有狀态就沒有傷害

并行執行無傷害

Copy-Paste重構代碼無傷害

函數的執行沒有順序上的問題

函數式程式設計還帶來了以下一些好處:

惰性求值:

這需要編譯器的支援,表達式不在它被綁定到變量之後就立即求值,而是在該值被取用的時候求值.

也就是說,語句如x:=expression;(把一個表達式的結果指派給一個變量)顯式地調用這個表達式被計算并把結果放置到x中,

但是先不管實際在x中的是什麼,直到通過後面的表達式中到x的引用而有了對它的值的需求的時候,而後面表達式自身的求值也可以被延遲,

最終為了生成讓外界看到的某個符号而計算這個快速增長的依賴樹.

确定性:

所謂确定性,就是像在數學中那樣,f(x)=y這個函數無論在什麼場景下,都會得到同樣的結果,而不是像程式中的很多函數那樣.同一個參數,在不同

的場景下會計算出不同的結果,這個我們稱之為函數的确定性.所謂不同的場景,就是我們的函數會根據運作中的狀态資訊的不同而發生變化.

We know,because of the "state",在并行執行和copy-paste時引發bug的機率是非常高的,是以沒有狀态就沒有傷害,就像沒有依賴就沒有傷害一樣.

并行執行無傷害,copy代碼無傷害,因為沒有狀态,代碼怎樣copy都行.

劣勢:

資料複制比較嚴重

有一些人可能會覺得這會對性能造成影響,其實,這個劣勢不見得會導緻性能不好.因為沒有狀态,是以代碼在并行上根本不需要鎖(不需要對狀态修改的鎖),

是以可以拼命地并發,反而可以讓性能很不錯. 比如: Erlang就是其中的代表.

對于純函數式(也就是完全沒有狀态的函數)的程式設計來說,各個語言支援的程度如下:

完全純函數式 Haskell

容易寫純函數 F#,Ocaml,Clojure,Sala

純函數需要花點精力 C#,Java,JavaScript

完全純函數的語言呢,很容易寫成函數,純函數需要花精力.隻要所謂的純函數的問題,傳進來的資料不改,改完的東西複制一份拷出去,然後沒有狀态顯示.

But most people 并不習慣函數式程式設計,因為函數式程式設計和過程式程式設計的思維方式完全不同.

過程式程式設計是在把具體的流程描述出來,是以可以不假思索,而函數式程式設計的抽象度更大,在實作方式上,有

函數套函數, 函數傳回函數, 函數裡定義函數 , 把人搞得confused

函數式程式設計用到的技術:

下面是函數式程式設計用到的一些技術:

first class function (頭等函數) :

這個技術可以讓你的函數就像變量一樣來使用. 也就是說,你的函數可以像變量一樣被建立,修改,并當成變量一樣傳遞,傳回.

或是在函數中嵌套函數.

tail recursion optimization (尾遞歸優化) :

遞歸的害處呢在于如果遞歸很深的話呢,stack受不了,并導緻性能大幅度下降,是以,我們使用尾遞歸優化技術--每次遞歸時都重用

stack,這樣能夠提升性能哦.當然這需要語言或編譯器的支援呢. Python 就不支援.

map & reduce :

這個技術不用多說了,函數式程式設計最常見的技術就是對一個集合做Map和Reduce操作了,這比起過程式的語言來說,在代碼上要更容易閱讀

(傳統過程式的語言需要使用for/while循環,然後在各種變量中把資料倒過來倒過去的)這個很像C++STL中foreach啊,find_if啊,count_if等函數的玩法.

pipeline (管道) :

這個技術的意思是,将函數執行個體成一個一個的action,然後将一組action放到一個數組或是清單中,再把資料傳給這個action list,資料就像一個pipeline一樣

順序地被各個函數所操作,最終得到我們想要的結果.

recursing (遞歸) :

遞歸最大的好處呢就是簡化代碼,它可以把一個複雜的問題用很簡單的代碼描述出來. 注意:遞歸的精髓是描述問題,而這正是函數式程式設計的精髓.

currying (柯裡化) :

将一個函數的多個參數分解成多個函數,然後将函數多層封裝起來,每層函數都傳回一個函數去接收下一個參數,這可以簡化函數的多個參數.

在C++中呢,這很像STL中的bind1st或是bind2nd.

higher order function (高階函數) :

所謂高階函數就是函數當參數,把傳入的函數做一個封裝,然後傳回這個封裝函數,現象上就是函數傳進傳出,就像面向對象對象滿天飛一樣.

這個技術用來做Decorator很不錯呢.

!!!!

函數式程式設計的思維方式

前面提到多次,函數式程式設計關注的是: describe what to do,rather than how to do it.

于是,我們把以前的過程式程式設計範式叫做 Imperative Programming - 指令式程式設計,而把函數式程式設計範式叫做 Declarative Programming - 聲明式程式設計.

傳統方式的寫法:

下面我們看一下相關的示例.

比如,我們有3輛車比賽,簡單起見,我們分别給這3輛車70%的機率讓它們可以往前走一步,一共5次機會.然後打出這一次這3輛車的前行狀态:

對于 Imperative Programming 來說,代碼如下: Python

經過函數子產品化: 更容易地閱讀代碼:

上面代碼,從主循環開始,可以清楚看到程式主幹,因為哦我們把程式的邏輯分成了幾個函數. 這樣一來呢

代碼邏輯就會變成幾個小碎片,于是我們讀代碼時要考慮上下文就少了很多,閱讀代碼也會更容易.

不像第一個示例.

而将代碼邏輯封裝成函數後,我們就相當于給每個相對獨立的程式邏輯取了個名字,于是代碼成了自解釋的.

但是你會發現哦,封裝成函數後,這些函數都會依賴于共享的變量來同步其狀态.

于是在讀代碼過程時,每當進入到函數裡,讀到通路了一個外部的變量時,我們馬上要去檢視這個變量的上下文,

然後還要在大腦裡推演這個變量的狀态,才能知道程式的真正邏輯.也就是說,

這些函數必須知道其它函數是怎樣修改它們之間共享變量的,是以這些函數是有狀态的.

函數式的寫法 見下篇:

函數式程式設計

繼續閱讀