本節(jié)的內容的有以下幾點:
一树肃、編程范式以及為什么要使用函數(shù)式編程?
二嘴瓤、什么是函數(shù)式編程
三、函數(shù)式編程的特征
一莉钙、編程范式以及為什么要使用函數(shù)式編程廓脆?
1、編程范式
我想大家應該在平時工作過程中磁玉,也許會因為項目而去另外學習或適應一種自己之前完全不熟悉的編程語言停忿,對有著一定編程經驗并已經熟練掌握一門語言的人來說,快速上手一門語言并應用于項目中也許并不是一件很困難的事情蚊伞。但是情況并非總是如此席赂,跨語言對一個程序員來說影響也許不是最大的,但是編程范式的變更也許會讓一個程序員好一會都緩不過神來时迫。
編程范式與編程語言不同颅停,它很深層更內在,它是編程思想的凝練掠拳,通過編程語言的體現(xiàn)出來癞揉,又通過實踐內化為程序員的一種編程思維。它并不容易在短時間內融匯貫通,而需要通過大量地實踐加深對這種編程方式的理解喊熟。
我們常見的主流編程思維有三種:
1柏肪、邏輯式編程
2、命令式編程
3芥牌、函數(shù)式編程
三種編程范式都體現(xiàn)了各自獨特的對用程序解決問題的思考烦味。
1、邏輯式編程不注重解決問題的步驟壁拉,而是注重邏輯谬俄。它設定答案須符合的規(guī)則來解決問題,而非設定步驟來解決問題:規(guī)則+事實=結果扇商。利用它編寫的程序不是由指令序列組成凤瘦,而是由一系列公理或定義對象之間關系的規(guī)則組。
2案铺、命令式編程關心解決問題的步驟蔬芥。它需要我們制定好對應解決某問題的一系列步驟,且讓程序嚴格按照步驟去執(zhí)行控汉。它編寫的程序需要我們去考慮在編碼范圍內需要考慮的一切問題笔诵,包括性能,邊界驗證姑子,資源回收等乎婿。
3、函數(shù)式編程關心的是數(shù)據(jù)的映射街佑,它重視更高層面上數(shù)據(jù)集之間的變換關系谢翎,而不是編制程序執(zhí)行的每一步笆搓。它在機器學習算法高度發(fā)展的今天午阵,變成了一種算法實現(xiàn)的主要編程范式之一胀莹。它的思維方式是將數(shù)據(jù)集的變換和數(shù)據(jù)上計算邏輯組合起來產生結果惊完。編碼者不需要過多關心數(shù)據(jù)集中每個元素的具體變換步驟报咳,只需要在數(shù)據(jù)集合上組織計算邏輯并觸發(fā)計算朋其。
二膝舅、什么是函數(shù)式編程
正如上面提到的姨伟,函數(shù)式編程是一種面向數(shù)據(jù)映射的編程范式谊迄,它的目標是使用純凈的函數(shù)來表達問題的解決方式闷供。
所謂函數(shù)式編程的函數(shù)本質,并不是指我們編程語言中的函數(shù)(例如python的def之類的)统诺,而是數(shù)學中的函數(shù)映射歪脏,這種映射只是接受參數(shù),并得到一個結果粮呢,它并不會對外界產生任何影響唾糯,這樣的一個函數(shù)的好處非常多怠硼,它們更有利于模塊化,因此更容易測試移怯、復用香璃、并行化、泛化以及推導舟误。
我們可以把函數(shù)對外界產生的影響稱之為副作用葡秒,下面一個例子來說明,帶有副作用的函數(shù)是如何造成困擾的嵌溢。
一個簡單的副作用例子
我們?yōu)橥婢叩曩徺I玩具來編寫一段程序眯牧,程序目的是購買一個玩具,并在信用卡上扣費
class Shop{
def buyToy(cc: CreditCart): Toy = {
val toy = Toy()
cc.charge(toy.price) // 副作用的源頭
toy
}
}
class CreditCart{
// deduct
def charge(price: Double) = ???
}
case class Toy(val price: Double = 10)
cc.charge(toy.price)就是副作用的源頭赖草,因為信用卡的計費可能會涉及到外部世界的一系列交互学少,我們的函數(shù)只不過想要返回一個玩具,而其它額外的行為也隨之發(fā)生了秧骑,這就是副作用版确。
這樣的副作用導致很難進行測試,因為我們不希望我們的測試方法真的去走一遍信用卡和外部交互的流程乎折。這種對可測試性的修改就意味著設計的修改:按理說CreditCard不應該知道如何去跟信用卡公司去進行實際扣費和持久化計費到內部系統(tǒng)中绒疗,我們可以讓CreditCard忽略這件事,通過一個Payments接口骂澄,與外部交互的邏輯都托管給這個實現(xiàn)這個Payments的對象吓蘑,然后分別實現(xiàn)一個為真正執(zhí)行計費邏輯的Payments和一個用于測試的MockPayments。這樣的做法使得模塊更加模塊化和可測試坟冲。
class Shop{
def buyToy(cc: CreditCart, p: Payments): Toy = {
val toy = Toy()
p.charge(cc, toy.price)
toy
}
}
trait Payments{
def charge(cc: CreditCart, price: Double)
}
我們這里再考慮一個問題:buyToy方法很難復用磨镶!例如一個客戶想要購買20個玩具,最理想的是復用這個方法健提,調用20次進行扣費琳猫,不管是從實際意義上的手續(xù)費角度,還是從支付系統(tǒng)的調用的性能方面都有十分不理的影響矩桂。當然沸移,我們還可以使用一個新的方法buyToys去實現(xiàn)痪伦,那么重復的代碼邏輯會很多侄榴,而且會失去代碼復用性和組合性。
去除副作用
函數(shù)式的解決方案就是去除副作用网沾,我們可以不需要在買玩具的時候把扣費的邏輯執(zhí)行了癞蚕,可以把這個費用本身和玩具一起返回,我們再來改造一下代碼:
class Shop{
def buyToy(cc: CreditCart): (Toy, Charge) = {
val toy = Toy()
(toy, Charge(cc, toy.price))
}
}
case class Charge(cc: CreditCart, amount: Double){
def combine(other: Charge): Charge = {
if(cc == other.cc)
Charge(cc, amount + other.amount)
else
throw new RuntimeException("不允許不同信用卡扣費")
}
}
在這段執(zhí)行邏輯中辉哥,我們并沒有在buyToy的方法中進行任何結算費用的操作桦山,而只是返回物品本身和它的費用攒射,我們希望的是把副作用剝離到更外層,而不是在函數(shù)調用的過程中進行恒水,那么我們的結算多個Toy的動作也就更好完成了会放。
def buyToys(cc: CreditCart, n: Int): (List[Toy], Charge) = {
val purchases : List[(Toy, Charge)] = List.fill(n)(buyToy(cc))
// List[(A, B)] => (List[A], List[B])
val (toys, charges) = purchases.unzip
(toys, charges.reduceLeft(_.combine(_))) // 合并消費
}
現(xiàn)在我們可以把購買玩具的邏輯和付賬邏輯隔離開,并可以復用代碼實現(xiàn)多個玩具的購買钉凌。
相比之前使用Payments接口而言咧最,我們使用Charge作為一等值的來隔離副作用,將
購買->付賬(副作用)->得到玩具
的邏輯轉變?yōu)?br>
(購買->得到賬單(可合并)->得到玩具)*->賬單一并結賬(副作用)
我們可以自己實現(xiàn)一個Payments對象在最后結算Charge里的price御雕,但是Toy類并不需要了解它矢沿。
買玩具小結
我們在這個例子中看到如何把計費的創(chuàng)建過程與實際的處理過程進行分離∷岣伲總的來說捣鲸,就是把這些副作用推到程序的外層,來轉化任何帶有副作用的函數(shù)闽坡。對于優(yōu)秀的函數(shù)式編程者來說栽惶,程序的實現(xiàn)就是一層純的內核和一層很薄的外圍來處理副作用。
三无午、函數(shù)式編程的特征
純函數(shù)
我們在前面提到過純函數(shù)的這一概念媒役,這里給出它的精確定義:如果一個函數(shù)在程序執(zhí)行的過程中出了根據(jù)輸入?yún)?shù)給出結果之外,對外界沒有任何其它的影響宪迟,那么可以說這一類函數(shù)是沒有副作用的酣衷,這類函數(shù)也稱為純函數(shù)。
例如Scala中1 + 2(+實際上是一個中置操作符次泽,可以被改寫為1.+(2))穿仪,那么函數(shù)+只接受2為參數(shù),然后與1相加返回一個新的整型3意荤,整個過程沒有引入到除了參數(shù)和調用者外的任意一個外界變化啊片。
引用透明和替代模型
純函數(shù)為函數(shù)式編程帶來的一個好處就是:純函數(shù)更容易推理,這就使得我們程序執(zhí)行的推導過程更為流暢和自然玖像。我們需要走到更高的層次去看看這些好處是怎么來的紫谷。
(為了敘述下面的內容,我們先來說明一下編碼層次:函數(shù)<表達式<程序捐寥。)
我們上升至表達式的領域來:對于1 + 2這個表達式笤昨,它在任何一個地方都可以被它的結果3直接取代而不會引起程序的任何變更,我們稱之為引用透明(表達式層面上的)握恳。當調用一個函數(shù)時傳入的表達式是引用透明的瞒窒,并且函數(shù)的調用也是引用透明的,那么這個函數(shù)就是一個純函數(shù)乡洼。純函數(shù)要求無論進行來任何操作都可以用它的返回值來代替它崇裁,這種限制使得程序的求值可以通過簡單自然的推導得出匕坯,我們稱之為替代模型(程序層面上的)。如果程序中每個表達式都是引用透明的拔稳,那么我們可以使用替代模型來進行等式推理葛峻,就例如我們的代數(shù)方程一般。
替代模型的之所以很容易進行推理巴比,因為它對運算的影響是局部的泞歉,只需要理解局部的計算邏輯,不需要在每一個表達式執(zhí)行過程中都縱觀全局的變化匿辩,對于程序的執(zhí)行可如對代數(shù)推理一般流暢而自然地進行腰耙。它使得程序進行模塊化變得十分簡單而清晰,而模塊化的函數(shù)更容易被測試和進一步的組合铲球,提供程序的整體質量挺庞。
小結
總的來說,函數(shù)式編程的相對于其它編程范式來說有著它獨特的優(yōu)勢稼病,尤其是對于我熟知的命令式范式來說选侨,它展現(xiàn)了一種完全不同的編程思維。在本章筆者也有一些對問題的思考:
去除了副作用之后然走,所有問題的都有一套函數(shù)式的編程方案嘛援制?
筆者認為,Scala在意的是芍瑞,如何進行函數(shù)式編程晨仑,并非所有問題的最佳方案都是使用函數(shù)式編程范式解決,函數(shù)式范式有自己的適用場景拆檬。
引用:
https://www.zhihu.com/question/28292740
《Scala函數(shù)式編程》