函數(shù)式編程(FP) 學(xué)習(xí)筆記
- 同大家分享、交流學(xué)習(xí)成果,觀點(diǎn)不一定正確椿猎,請大家用辯證的眼光來看待本文內(nèi)容
- 鳥瞰函數(shù)式編程的概況
- 簡單了解一下函數(shù)式編程中的基本脈絡(luò)和一些主要思想
- 看看大家怎么說
- 本文中的例子均使用javascript或swfit
1.
"函數(shù)式編程" 是一種 "編程范式"(programming paradigm)
也就是如何編寫程序的方法論
In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data.
函數(shù)式編程是一種編程模型,他將計(jì)算機(jī)運(yùn)算看做是數(shù)學(xué)中函數(shù)的計(jì)算,并且避免了狀態(tài)以及變量的概念
FP屬于"結(jié)構(gòu)化編程"的一種暖庄,主要思想是把運(yùn)算過程盡量寫成一系列嵌套的函數(shù)調(diào)用;
模塊化是成功編程的關(guān)鍵楼肪,而函數(shù)編程可以極大地改進(jìn)模塊化培廓。
函數(shù)式編程的思想早已遍地開花,例如下面這些巨著:
2.
函數(shù)式編程的特性
- 閉包和高階函數(shù)
- 惰性計(jì)算
- 遞歸
閉包
閉包是自包含的函數(shù)代碼塊春叫,可以在代碼中被傳遞和使用肩钠。(自包含:組件不依賴其他組件俘侠,能夠以獨(dú)立的方式供外部使用。)
閉包就是引用了自由變量的函數(shù)蔬将。
這個被引用的自由變量將和這個函數(shù)一同存在爷速,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。
如何來理解這個自由變量呢霞怀?
自由變量是指在函數(shù)中使用的惫东,但既不是函數(shù)參數(shù)也不是函數(shù)的局部變量的變量
什么樣的變量是自由變量呢?
如下片斷中的freeVar就是個自由變量:
//javascript
function wrapper() {
var freeVar = 42;
function inner() {
return 2 * freeVar;
}
return inner;
自由變量在閉包生成之前毙石,并不是函數(shù)的一部分廉沮。在函數(shù)被調(diào)用時,閉包才會形成徐矩,函數(shù)將這個自由變量納入自己的作用域滞时,也就是說,自由變量從此與定義它的容器無關(guān)滤灯,以函數(shù)被調(diào)用那一刻為時間點(diǎn)坪稽,成為函數(shù)Context中的成員。
# 例1
//swift
//定義一個求和閉包
//閉包類型:(Int,Int)->(Int)
var c = 100
let add:(Int,Int)->(Int) = {
(a,b) in
return a + b + c
}
//執(zhí)行閉包
let result = add(1,2)
//打印閉包返回值
print("result=\(result)")
----
# 例2
//swift
//包含一個自由變量的閉包
func makeIncrementor(forIncrement amount:Int)->()->Int{
var runningTotal = 0//這是一個自由變量
func incrementor()->Int{
runningTotal += amount
return runningTotal
}
return incrementor
}
let incrementByTen = makeIncrementor(forIncrement:10)
incrementByTen()//10
incrementByTen()//20
incrementByTen()//30
高階函數(shù)
在 Wikipedia 中鳞骤,是這么定義高階函數(shù)(higher-order function)的窒百,如果一個函數(shù):
- 接受一個或多個函數(shù)當(dāng)作參數(shù)
- 或者,把一個函數(shù)當(dāng)作返回值
那么這個函數(shù)就被稱作高階函數(shù)豫尽。
//swift
import UIKit
var usernames = ["Lves", "Wildcat", "Cc", "Lecoding"]
func backWards(s1: String, s2: String) -> Bool
{
return s1 > s2
}
var resultName1 = usernames.sorted(by:backWards)
//結(jié)果:resultName1: ["Wildcat", "Lves", "Lecoding", "Cc"]
惰性計(jì)算
惰性計(jì)算(Lazy Evaluation)篙梢,目標(biāo):最小化計(jì)算機(jī)要做的工作。
兩層含義:“延遲計(jì)算”和“短路求值”
在使用延遲求值的時候美旧,表達(dá)式不在它被綁定到變量之后就立即求值渤滞,而是在該值被取用的時候求值。
例如:
x:=expression; (把一個表達(dá)式的結(jié)果賦值給一個變量x)
從字面意義上來看榴嗅,調(diào)用這個表達(dá)式妄呕,將立即把被計(jì)算結(jié)果放置到 x 中,
但是在惰性計(jì)算中录肯,則不會先立即進(jìn)行計(jì)算趴腋,而是直到在后續(xù)表達(dá)式中對 x 的引用有取值的需求時才進(jìn)行計(jì)算。而后續(xù)表達(dá)式自身的求值也可以被延遲论咏,最終為了生成讓外界看到的某個符號而計(jì)算這個快速增長的依賴樹优炬。
來看一個困惑前端的示例, 使用Javascript循環(huán)添加事件:
<meta charset="UTF-8">
<button>第1條記錄</button>
<button>第2條記錄</button>
<button>第3條記錄</button>
<button>第4條記錄</button>
<button>第5條記錄</button>
<button>第6條記錄</button>
<script type="text/javascript">
//test 1 沒有使用閉包
var buttonst_obj = document.getElementsByTagName("button");
for (var i = 0, len = buttonst_obj.length; i < len; i++) {
buttonst_obj[i].onclick = function() {
alert(i);
};
}
上述片斷的結(jié)果是:每個Button彈出的都是6,因?yàn)闆]有形成有效的閉包厅贪。
閉包是有延遲求值特性的蠢护,所以在函數(shù)得到執(zhí)行時,i === 6养涮。(循環(huán)執(zhí)行完成后葵硕,onclick事件才觸發(fā)眉抬,此時i已經(jīng)=6了)
如果我們將它改成下面這樣,讓 i 作為外層函數(shù)的參數(shù)而被內(nèi)層函數(shù)閉包懈凹,結(jié)果則是我們想要的:
//test 2 使用閉包
var buttonst_obj = document.getElementsByTagName("button");
for (var i = 0, len = buttonst_obj.length; i < len; i++) {
buttonst_obj[i].onclick = clickEvent(i);
}
function clickEvent(i){
return function () {
alert(i);
}
}
</script>
Why蜀变?
因?yàn)檫@個clickEvent(i) 高階函數(shù),
它將 i 作為自由變量傳遞(注意:i 并不是內(nèi)函數(shù)的參數(shù)介评,也不是內(nèi)函數(shù)的一部分)库北,
在 click 時閉包已經(jīng)形成并被傳遞。
短路求值
作為"&&"和"||"操作符的操作數(shù)表達(dá)式们陆,這些表達(dá)式在進(jìn)行求值時寒瓦,只要最終的結(jié)果已經(jīng)可以確定是真或假,求值過程便告終止坪仇,這稱之為短路求值(short-circuit evaluation)杂腰。這是這兩個操作符的一個重要屬性。
遞歸 (略)
3.
函數(shù)式編程還具有以下五個鮮明的特點(diǎn)
- 函數(shù)是"第一等公民"
- 只用"表達(dá)式"椅文,不用"語句"
- 沒有"副作用"
- 不修改狀態(tài)
- 引用透明性
函數(shù)是"第一等公民"
所謂"第一等公民"(first class)喂很,指的是函數(shù)與其他數(shù)據(jù)類型一樣,處于平等地位雾袱,可以賦值給其他變量恤筛,也可以作為參數(shù),傳入另一個函數(shù)芹橡,或者作為別的函數(shù)的返回值。
舉個栗子:下面代碼中的cprint函數(shù)望伦,可以作為另一個函數(shù)的參數(shù)林说。
//swift
func cprint(i:Int){
print(i)
}
let arr = [1, 2, 4]
arr.forEach(cprint)
只用"表達(dá)式",不用"語句"
"表達(dá)式"(expression)是一個單純的運(yùn)算過程屯伞,總是有返回值腿箩;
"語句"(statement)是執(zhí)行某種操作,沒有返回值劣摇。
函數(shù)式編程要求珠移,只使用表達(dá)式,不使用語句末融。
也就是說钧惧,每一步都是單純的運(yùn)算,而且都有返回值勾习。
函數(shù)式編程的動機(jī)浓瞪,一開始就是為了處理運(yùn)算(computation),不考慮系統(tǒng)的讀寫(I/O)巧婶。
"語句"屬于對系統(tǒng)的讀寫操作乾颁,所以就被排斥在外涂乌。
當(dāng)然,實(shí)際應(yīng)用中英岭,不做I/O是不可能的湾盒。
因此,編程過程中诅妹,函數(shù)式編程只要求把I/O限制到最小罚勾,不要有不必要的讀寫行為,保持計(jì)算過程的單純性漾唉。
沒有"副作用"
所謂"副作用"(side effect)荧库,指的是函數(shù)內(nèi)部與外部互動(最典型的情況,就是修改全局變量的值)赵刑,產(chǎn)生運(yùn)算以外的其他結(jié)果分衫。
函數(shù)式編程強(qiáng)調(diào)沒有"副作用",意味著函數(shù)要保持獨(dú)立般此,所有功能就是返回一個新的值蚪战,沒有其他行為,尤其是不得修改外部變量的值铐懊。
不修改狀態(tài)
變量往往用來保存"狀態(tài)"(state)邀桑。
不修改變量,意味著狀態(tài)不能保存在變量中科乎。
函數(shù)式編程使用參數(shù)保存狀態(tài)壁畸,最好的例子就是遞歸。
舉個栗子:計(jì)算階乘 n!
//swift
//使用循環(huán)的方式來計(jì)算階乘茅茂,total變量用于保存臨時狀態(tài)
func cJieChen(n:Int) -> Int {
var total = 1
for i in 1...n{
total = total * i
}
return total
}
var t = cJieChen(n:4)
//swift
//使用遞歸方式計(jì)算階乘捏萍,用參數(shù)來傳遞狀態(tài)
func jiechen(n:Int) -> Int {
if(n==0){
return 1
}else{
return n * jiechen(n:n-1)
}
}
var a = jiechen(n: 4)
引用透明性
函數(shù)式編程還能夠增強(qiáng)引用透明性,
即如果提供同樣的輸入空闲,那么函數(shù)總是返回同樣的結(jié)果令杈。
也就是說,
表達(dá)式的值不依賴于可以改變的值的全局狀態(tài)碴倾。
這使您可以從形式上推斷程序行為逗噩,
因?yàn)楸磉_(dá)式的意義只取決于其子表達(dá)式而不是計(jì)算順序或者其他表達(dá)式的副作用。
這有助于驗(yàn)證正確性跌榔、簡化算法异雁,甚至有助于找出優(yōu)化它的方法。
副作用
副作用指的是修改系統(tǒng)狀態(tài)的語言結(jié)構(gòu)矫户。
因?yàn)?FP 語言不包含任何賦值語句片迅,變量值一旦被指派就永遠(yuǎn)不會改變。而且皆辽,調(diào)用函數(shù)只會計(jì)算出結(jié)果 ── 不會出現(xiàn)其他效果柑蛇。因此芥挣,F(xiàn)P 語言沒有副作用。
4.
函數(shù)式編程好處
- 代碼簡潔耻台,開發(fā)快速
- 接近自然語言空免,易于理解
- 更方便的代碼管理
- 易于"并發(fā)編程"
- 代碼的熱升級
代碼簡潔,開發(fā)快速
函數(shù)式編程大量使用函數(shù)盆耽,減少了代碼的重復(fù)蹋砚,因此程序比較短,開發(fā)速度較快
接近自然語言摄杂,易于理解
函數(shù)式編程的自由度很高坝咐,可以寫出很接近自然語言的代碼。
例如:mansory中的鏈?zhǔn)秸{(diào)用析恢;
更方便的代碼管理
函數(shù)式編程不依賴墨坚、也不會改變外界的狀態(tài),只要給定輸入?yún)?shù)映挂,返回的結(jié)果必定相同泽篮。因此,每一個函數(shù)都可以被看做獨(dú)立單元柑船,很有利于進(jìn)行單元測試(unit testing)和除錯(debugging)帽撑,以及模塊化組合。
易于"并發(fā)編程"
函數(shù)式編程不需要考慮"死鎖"(deadlock)鞍时,因?yàn)樗恍薷淖兞靠骼愿静淮嬖?鎖"線程的問題。不必?fù)?dān)心一個線程的數(shù)據(jù)逆巍,被另一個線程修改专筷,所以可以很放心地把工作分?jǐn)偟蕉鄠€線程,部署"并發(fā)編程"(concurrency)蒸苇。
多核CPU是將來的潮流,所以函數(shù)式編程的這個特性非常重要吮旅。
代碼的熱升級
函數(shù)式編程沒有副作用溪烤,只要保證接口不變,內(nèi)部實(shí)現(xiàn)是外部無關(guān)的庇勃。所以檬嘀,可以在運(yùn)行狀態(tài)下直接升級代碼,不需要重啟责嚷,也不需要停機(jī)鸳兽。Erlang語言早就證明了這一點(diǎn),它是瑞典愛立信公司為了管理電話系統(tǒng)而開發(fā)的罕拂,電話系統(tǒng)的升級當(dāng)然是不能停機(jī)的揍异。
5.
顧慮
函數(shù)式編程常被認(rèn)為嚴(yán)重耗費(fèi)在CPU和存儲器資源
- 早期的函數(shù)式編程語言實(shí)現(xiàn)時并無考慮過效率問題全陨。
- 有些非函數(shù)式編程語言為求提升速度,不提供自動邊界檢查或自動垃圾回收等功能衷掷。
6.
柯里化(Currying)
柯里化(Currying)辱姨,又稱部分求值(Partial Evaluation),是一種函數(shù)式編程思想戚嗅,就是把接受多個參數(shù)的函數(shù)轉(zhuǎn)換成接收一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù)雨涛,并且返回一個接受余下參數(shù)的新函數(shù)技術(shù)。
Currying wiki
例3
在swift中利用閉包進(jìn)行柯里化
//swift
//假設(shè)原先有一個下面這樣的函數(shù)
func addOrignal(_ a1:Int,_ a2:Int) -> Int {
return a1+a2
}
let a1 = addOrignal(2,3)
print("a1=\(a1)")
//使用柯里化的方式進(jìn)行改造
/**
這個栗子中的4行代碼懦胞,講述的大概是下面的故事:
(1)函數(shù)addTo的返回值為 (Int) -> Int :記作R替久,R仍然是一個函數(shù);
(2)函數(shù)addTo接收一個數(shù)字類型的參數(shù)(adder)躏尉;
(3)返回函數(shù)(R)接收一個數(shù)字類型的參數(shù)(num)蚯根;
(4)返回函數(shù)(R)的返回值是一個數(shù)值,這個數(shù)值為:num+adder
(5)adder是函數(shù)addTo的參數(shù)醇份,而num是函數(shù)R的參數(shù)稼锅,當(dāng)函數(shù)R被創(chuàng)建后,參數(shù)adder被閉包到函數(shù)R當(dāng)中僚纷,并參與函數(shù)R的運(yùn)算矩距;
*/
func addTo(_ adder:Int) -> (Int) -> Int {
return {
num in
return num + adder
}
}
let add2 = addTo(2)
let a2 = add2(3)
print("a2=\(a2)");
//a2=4
let add3 = addTo(3)
let a3 = add3(3)
print("a3=\(a3)");
//a3=6
Map & Reduce
MapReduce是一種編程模型,用于大規(guī)模數(shù)據(jù)集(大于1TB)的并行運(yùn)算怖竭。概念"Map(映射)"和"Reduce(歸約)"锥债,是它們的主要思想,都是從函數(shù)式編程語言里借來的痊臭,還有從矢量編程語言里借來的特性哮肚。它極大地方便了編程人員在不會分布式并行編程的情況下,將自己的程序運(yùn)行在分布式系統(tǒng)上广匙。 當(dāng)前的軟件實(shí)現(xiàn)是指定一個Map(映射)函數(shù)允趟,用來把一組鍵值對映射成一組新的鍵值對,指定并發(fā)的Reduce(歸約)函數(shù)鸦致,用來保證所有映射的鍵值對中的每一個共享相同的鍵組潮剪。
pipeline
這個技術(shù)的意思是,把函數(shù)實(shí)例成一個一個的action分唾,然后抗碰,把一組action放到一個數(shù)組或是列表中,然后把數(shù)據(jù)傳給這個action list绽乔,數(shù)據(jù)就像一個pipeline一樣順序地被各個函數(shù)所操作弧蝇,最終得到我們想要的結(jié)果。
7.
Lambda表達(dá)式
“Lambda 表達(dá)式”(lambda expression)是一個匿名函數(shù),Lambda表達(dá)式基于數(shù)學(xué)中的λ演算得名看疗,直接對應(yīng)于其中的lambda抽象(lambda abstraction)沙峻,是一個匿名函數(shù),即沒有函數(shù)名的函數(shù)鹃觉。Lambda表達(dá)式可以表示閉包(注意和數(shù)學(xué)傳統(tǒng)意義上的不同)专酗。
//swift
var resultName2 = usernames.sorted() {
(s1: String, s2: String) -> Bool in
return s1 < s2
}
sorted方法使用匿名閉包函數(shù)
8.
反面的觀點(diǎn)
不論是面向?qū)ο缶幊踢€是函數(shù)式編程,如果你走了極端盗扇,那都是錯誤的祷肯。
面向?qū)ο缶幊痰臉O端是一切都是對象(純面向?qū)ο?。
函數(shù)式編程的極端是純函數(shù)式編程語言疗隶。
為什么說面向?qū)ο缶幊毯秃瘮?shù)式編程都有問題(30)
反人類
難找工作
好多語言設(shè)計(jì)得不real world佑笋,用來解決實(shí)際問題很無(sha)力(bi)
學(xué)好函數(shù)式編程是要數(shù)學(xué)基礎(chǔ)的,尤其是了解數(shù)學(xué)是如何從更高層面抽象已經(jīng)被抽象過的東西斑鼻。
學(xué)的人容易感到智商不夠用
union-find算法的Dr. Harrop說:目前我們還沒有發(fā)現(xiàn)一個有效率的純函數(shù)的union-find集合蒋纬。
也就是說:對于有狀態(tài)的操作命令式操作會比聲明式操作更有效率。
參考資料
(2)百度百科
(5)Currying