淺析函數(shù)式編程
函數(shù)式編程
1. 函數(shù)
高中一年級,應(yīng)該是最早接觸函數(shù)這個概念的時間,印象很深刻,畢竟是高考壓軸大題,但它卻是必修一第二章的內(nèi)容。
我們來看一個必修一中最簡單的一個函數(shù):
上面的函數(shù)由三個部分組成:
- x 自變量,由它決定初始值
- f 它代表一個規(guī)則,用來對 自變量 進行計算。它也是用來描述函數(shù)關(guān)系的。
- y 因變量,它用來標(biāo)識通過函數(shù)計算以后的結(jié)果
一個函數(shù)的定義是:「一個自變量 x ,經(jīng)過一個規(guī)則 f(x) 的運算,得到一個因變量。其中的規(guī)則 f(x) 稱為函數(shù)」
上面的函數(shù)在數(shù)學(xué)上有一個術(shù)語叫「一元函數(shù)」
如上,它被稱為「二元函數(shù)」,自變量的多少決定了這個函數(shù)的名稱。
函數(shù)的概念,也被引入了計算機的領(lǐng)域中。很多的語言內(nèi)置了函數(shù)的語法,幫助我們?nèi)崿F(xiàn)類似于數(shù)學(xué)中函數(shù)的功能。
我們來看一個函數(shù)的定義
int?main(int?x)?{
?return?x?+?1;?
}
這是 C 語言中定義一個函數(shù)的語法規(guī)則,可以看出它和數(shù)學(xué)中的函數(shù)完全一樣,它傳入了一個自變量 x 在計算機中,它叫「參數(shù)」,同時在函數(shù)內(nèi)部,它對這個參數(shù)做了加法運算,并將其返回。此時這個加法運算就是數(shù)學(xué)中的規(guī)則 f(x) ,返回值就是因變量 y
類似,上面的函數(shù)叫一元函數(shù),因為他只有一個參數(shù)。
同理,兩個參數(shù)的,就稱為二元函數(shù),以此類推。
各種編程語言,提供了多種多樣的函數(shù)的定義方式,但其本質(zhì)和上面的函數(shù)完全一樣,只是定義方式發(fā)生了變化而已。
- Java
public?static?int?method(int?x)?{
??return?x?+?1;
}
- scala
def?method(x:?Int):?Int?=?{
??x?+?1
}
- javascript
function?method(x)?{
??return?x?+?1
}
2. 面向?qū)ο缶幊毯秃瘮?shù)式編程
寫 OOP 的人都有一個體會,以類作為最小的調(diào)度單元,實現(xiàn)一個功能,需要去「定義一些數(shù)據(jù)結(jié)構(gòu)和操作這些數(shù)據(jù)結(jié)構(gòu)的方法」。
也基于此,衍生出了設(shè)計模式這個代碼復(fù)用的規(guī)則。設(shè)計模式的出現(xiàn)就是為了解決 OOP 帶來的一些弊端,一定程度上實現(xiàn)對方法級別的重用。
函數(shù)式編程(后文以 FP 代替)講究不變性,如同一個數(shù)學(xué)函數(shù)一樣,只要你的入?yún)⑾嗤?,那么你的返回值必然相同,這樣做的好處在于,「這個函數(shù)對你的代碼沒有任何副作用,它不會更改所有的變量,只會返回一個新的變量,這也意味著它沒有線程安全的問題?!?/strong>
了解過一些支持 FP 的同學(xué),一定在相關(guān)的書籍上看到過一句話:「函數(shù)是一等公民」,支持 FP 的語言,將函數(shù)作為一種數(shù)據(jù)類型而存在。
而當(dāng)函數(shù)成為一種數(shù)據(jù)類型的時候,很多我們經(jīng)常使用到的設(shè)計模式也就有了其他的一些玩法。
3. 函數(shù)式編程下的設(shè)計模式
策略
策略設(shè)計模式,用來解決參數(shù)相同場景下的 if|else 的問題,直接看類圖
- Strategy(策略),定義所有支持的算法的公共接口。
- ConcreteStrategy(具體策略,如 SimpleCompository , TeXCompositor)
- Context (上下文,用來對具體的策略進行切換)
上面是典型的 OOP 的思路,我們來看一下 FP 下的代碼實現(xiàn)
function?test01(func)?{
??func("HelloWorld")
}
function?test02(str)?{
??log.info(str)
}
test01(test02())
模版方法
模版方法,預(yù)留一些擴展(方法)留給子類自己實現(xiàn),如生命周期函數(shù)。來看類圖

FP函數(shù)式代碼實現(xiàn):
function?test01(doCreateDocument,?aboutToOpenDocument)?{
??log.info("start")
??doCreateDoCument(str)
??aboutToOpenDocument(str)
}
test01(data?=>?{
??
},?error?=>?{
??
})
可以發(fā)現(xiàn)無論是策略還是模版設(shè)計模式,都在使用函數(shù)作為數(shù)據(jù)類型,進而代替了 OOP 中的繼承的作用。但對于一個函數(shù)而言,參數(shù)的個數(shù)問題成為了一個問題,實現(xiàn)一個功能我們可以依賴于外部的多個參數(shù),此時一味的進行傳參,對于后續(xù)代碼維護、擴展都有很大的影響,于此函數(shù)式編程的一個特性也隨之誕生。
4. 柯里化
閉包
閉包的概念來自于前端,通俗的話來講,「閉包就是引用了一個函數(shù)內(nèi)部所有變量(包括參數(shù))的一個組合?!?/strong>
閉包在函數(shù)創(chuàng)建的時候就會被默認創(chuàng)建,如同類的構(gòu)造函數(shù)。
?一個問題:沒有返回值的函數(shù)存在閉包嗎?
答:存在,沒有使用而已
?
上面提到了一個多參數(shù)的問題,我們來看一段代碼
function?func1(str1)?{
??return?func2(str2)?{
????return?str1?===?str2
??}
}
const?func3?=?func1("1")
const?ans?=?func3("2")
可以看到,func1 返回了一個函數(shù) func2 ,func2 函數(shù)訪問了 func1 函數(shù)的參數(shù)。這個的實現(xiàn)就依賴于 「閉包」。
func3 獲取到了 func2 的一個引用。此時繼續(xù)調(diào)用 func3 ,得到 func1 最終的處理結(jié)果。
上面將函數(shù)調(diào)用分為了兩步,如果連在一起寫就是 func1("1")("2")。
?Func1(1)(2) 這樣寫的好處在哪里?
?
-
將一個多元函數(shù)拆分為多個低元函數(shù),參數(shù)之間可以進行預(yù)處理,然后進行整合;
-
一元函數(shù)方便復(fù)用
這種變形調(diào)用方式,在函數(shù)式編程中存在一個術(shù)語「柯里化」。
5. Java 中的函數(shù)式
從 1.8 開始 Jdk 從語言層面提供了一些能力用以在 Java 領(lǐng)域書寫一些函數(shù)式編程。
函數(shù)作為參數(shù)
Java 1.8 以后提供了一個注解 @FunctionalInterface 。它的定義是:內(nèi)部僅僅存在一個抽象方法的接口,即可聲明為函數(shù)式接口。同時也提供了一些通用的類,來實現(xiàn)函數(shù)式編程。

圖中最上面是四個基本的函數(shù)式接口,Consumer, Predicate, Supplier 三個類都是對 Function 類的一次封裝,Consumer 類沒有返回值,Predicate 返回值為 「布爾值」 , Supplier 類沒有參數(shù)。而 Function 類是一個標(biāo)準(zhǔn)的函數(shù)式接口,看一下它的定義。
@FunctionalInterface
public?interface?Function<T,?R>?{
????/**
?????*?Applies?this?function?to?the?given?argument.
?????*
?????*?@param?t?the?function?argument
?????*?@return?the?function?result
?????*/
????R?apply(T?t);
??
???default?<V>?Function<V,?R>?compose(Function<??super?V,???extends?T>?before)?{
????????Objects.requireNonNull(before);
????????return?(V?v)?->?apply(before.apply(v));
????}
???default?<V>?Function<T,?V>?andThen(Function<??super?R,???extends?V>?after)?{
????????Objects.requireNonNull(after);
????????return?(T?t)?->?after.apply(apply(t));
????}
??
}
可以看到,他內(nèi)部提供了一個抽象函數(shù), R apply(T t) 接受一個泛型,返回一個泛型。「這是最標(biāo)準(zhǔn)的一元函數(shù)(只有一個參數(shù))?!?/strong>
那么問題來了,如果我需要兩個參數(shù)怎么辦,JDK,提供了一個 BiFunction 可以接受兩個參數(shù),當(dāng)然只能返回一個值,如果想返回多個值,請封裝一個 集合類型。
那么問題又來了,如果我需要三個,或者更多的參數(shù)怎么辦,不可能 JDK 將所有的參數(shù)個數(shù)的函數(shù)都封裝一遍吧。
我們來思考一下這個問題的解決方法:對于多元(多參)的函數(shù),我們能否將它們拆成一個個的一元函數(shù),然后讓這個一元函數(shù)返回一個一元函數(shù),來實現(xiàn)多參數(shù)的傳遞。這就是我們前面提到的柯里化的思想。
來看一下具體的實現(xiàn)
public?void?test()?{
????????System.out.println(test01(param01?->?param02?->?param03?->?param01?+?param02?+?param03));
????}
????public?String?test01(Function<String,?Function<String,?Function<String,?String>>>?function)?{
????????//?condition
????????String?method01?=?"111";
????????String?method02?=?"222";
????????String?method03?=?"333";
????????return?function.apply(method01).apply(method02).apply(method03);
????}
通過以上的方式可以簡單的去寫一些服用代碼,比如返回值相同但是調(diào)用方方不同的第三方調(diào)用。
以上就是對函數(shù)式編程的一個簡單的介紹,具體的其他行為需要在真正的編碼中進行實踐。
往 期 原 創(chuàng) 好 文 推 薦 :
抽
絲
剝
繭
—
—
狀
態(tài)
設(shè)
計
模
式
抽
絲
剝
繭
—
—
模
板
方
法
設(shè)
計
模
式
抽
絲
剝
繭
—
—
迭
代
器
設(shè)
計
模
式
T
h
r
e
a
d
L
o
c
a
l
為
啥
要
用
弱
引
用
?
不
知
道
