Scala函數(shù)式編程之一——編程范式

本節(jié)的內(nèi)容的有以下幾點:
一、編程范式以及為什么要使用函數(shù)式編程?
二、什么是函數(shù)式編程
三、函數(shù)式編程的特征

一、編程范式以及為什么要使用函數(shù)式編程?

1、編程范式

我想大家應(yīng)該在平時工作過程中,也許會因為項目而去另外學習或適應(yīng)一種自己之前完全不熟悉的編程語言,對有著一定編程經(jīng)驗并已經(jīng)熟練掌握一門語言的人來說,快速上手一門語言并應(yīng)用于項目中也許并不是一件很困難的事情。但是情況并非總是如此,跨語言對一個程序員來說影響也許不是最大的,但是編程范式的變更也許會讓一個程序員好一會都緩不過神來。

編程范式與編程語言不同,它很深層更內(nèi)在,它是編程思想的凝練,通過編程語言的體現(xiàn)出來,又通過實踐內(nèi)化為程序員的一種編程思維。它并不容易在短時間內(nèi)融匯貫通,而需要通過大量地實踐加深對這種編程方式的理解。

我們常見的主流編程思維有三種:
1、邏輯式編程
2、命令式編程
3、函數(shù)式編程

三種編程范式都體現(xiàn)了各自獨特的對用程序解決問題的思考。
1、邏輯式編程不注重解決問題的步驟,而是注重邏輯。它設(shè)定答案須符合的規(guī)則來解決問題,而非設(shè)定步驟來解決問題:規(guī)則+事實=結(jié)果。利用它編寫的程序不是由指令序列組成,而是由一系列公理或定義對象之間關(guān)系的規(guī)則組。
2、命令式編程關(guān)心解決問題的步驟。它需要我們制定好對應(yīng)解決某問題的一系列步驟,且讓程序嚴格按照步驟去執(zhí)行。它編寫的程序需要我們?nèi)タ紤]在編碼范圍內(nèi)需要考慮的一切問題,包括性能,邊界驗證,資源回收等。
3、函數(shù)式編程關(guān)心的是數(shù)據(jù)的映射,它重視更高層面上數(shù)據(jù)集之間的變換關(guān)系,而不是編制程序執(zhí)行的每一步。它在機器學習算法高度發(fā)展的今天,變成了一種算法實現(xiàn)的主要編程范式之一。它的思維方式是將數(shù)據(jù)集的變換和數(shù)據(jù)上計算邏輯組合起來產(chǎn)生結(jié)果。編碼者不需要過多關(guān)心數(shù)據(jù)集中每個元素的具體變換步驟,只需要在數(shù)據(jù)集合上組織計算邏輯并觸發(fā)計算。

二、什么是函數(shù)式編程

正如上面提到的,函數(shù)式編程是一種面向數(shù)據(jù)映射的編程范式,它的目標是使用純凈的函數(shù)來表達問題的解決方式。

所謂函數(shù)式編程的函數(shù)本質(zhì),并不是指我們編程語言中的函數(shù)(例如python的def之類的),而是數(shù)學中的函數(shù)映射,這種映射只是接受參數(shù),并得到一個結(jié)果,它并不會對外界產(chǎn)生任何影響,這樣的一個函數(shù)的好處非常多,它們更有利于模塊化,因此更容易測試、復用、并行化、泛化以及推導。

我們可以把函數(shù)對外界產(chǎn)生的影響稱之為副作用,下面一個例子來說明,帶有副作用的函數(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ā)生了,這就是副作用。

這樣的副作用導致很難進行測試,因為我們不希望我們的測試方法真的去走一遍信用卡和外部交互的流程。這種對可測試性的修改就意味著設(shè)計的修改:按理說CreditCard不應(yīng)該知道如何去跟信用卡公司去進行實際扣費和持久化計費到內(nèi)部系統(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個玩具,最理想的是復用這個方法,調(diào)用20次進行扣費,不管是從實際意義上的手續(xù)費角度,還是從支付系統(tǒng)的調(diào)用的性能方面都有十分不理的影響。當然,我們還可以使用一個新的方法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的方法中進行任何結(jié)算費用的操作,而只是返回物品本身和它的費用,我們希望的是把副作用剝離到更外層,而不是在函數(shù)調(diào)用的過程中進行,那么我們的結(jié)算多個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作為一等值的來隔離副作用,將
購買->付賬(副作用)->得到玩具
的邏輯轉(zhuǎn)變?yōu)?br> (購買->得到賬單(可合并)->得到玩具)*->賬單一并結(jié)賬(副作用)

我們可以自己實現(xiàn)一個Payments對象在最后結(jié)算Charge里的price,但是Toy類并不需要了解它。

買玩具小結(jié)

我們在這個例子中看到如何把計費的創(chuàng)建過程與實際的處理過程進行分離??偟膩碚f,就是把這些副作用推到程序的外層,來轉(zhuǎn)化任何帶有副作用的函數(shù)。對于優(yōu)秀的函數(shù)式編程者來說,程序的實現(xiàn)就是一層純的內(nèi)核和一層很薄的外圍來處理副作用。

三、函數(shù)式編程的特征

純函數(shù)

我們在前面提到過純函數(shù)的這一概念,這里給出它的精確定義:如果一個函數(shù)在程序執(zhí)行的過程中出了根據(jù)輸入?yún)?shù)給出結(jié)果之外,對外界沒有任何其它的影響,那么可以說這一類函數(shù)是沒有副作用的,這類函數(shù)也稱為純函數(shù)。

例如Scala中1 + 2(+實際上是一個中置操作符,可以被改寫為1.+(2)),那么函數(shù)+只接受2為參數(shù),然后與1相加返回一個新的整型3,整個過程沒有引入到除了參數(shù)和調(diào)用者外的任意一個外界變化。

引用透明和替代模型

純函數(shù)為函數(shù)式編程帶來的一個好處就是:純函數(shù)更容易推理,這就使得我們程序執(zhí)行的推導過程更為流暢和自然。我們需要走到更高的層次去看看這些好處是怎么來的。

(為了敘述下面的內(nèi)容,我們先來說明一下編碼層次:函數(shù)<表達式<程序。)

我們上升至表達式的領(lǐng)域來:對于1 + 2這個表達式,它在任何一個地方都可以被它的結(jié)果3直接取代而不會引起程序的任何變更,我們稱之為引用透明(表達式層面上的)。當調(diào)用一個函數(shù)時傳入的表達式是引用透明的,并且函數(shù)的調(diào)用也是引用透明的,那么這個函數(shù)就是一個純函數(shù)。純函數(shù)要求無論進行來任何操作都可以用它的返回值來代替它,這種限制使得程序的求值可以通過簡單自然的推導得出,我們稱之為替代模型(程序?qū)用嫔系模?。如果程序中每個表達式都是引用透明的,那么我們可以使用替代模型來進行等式推理,就例如我們的代數(shù)方程一般。

替代模型的之所以很容易進行推理,因為它對運算的影響是局部的,只需要理解局部的計算邏輯,不需要在每一個表達式執(zhí)行過程中都縱觀全局的變化,對于程序的執(zhí)行可如對代數(shù)推理一般流暢而自然地進行。它使得程序進行模塊化變得十分簡單而清晰,而模塊化的函數(shù)更容易被測試和進一步的組合,提供程序的整體質(zhì)量。

小結(jié)

總的來說,函數(shù)式編程的相對于其它編程范式來說有著它獨特的優(yōu)勢,尤其是對于我熟知的命令式范式來說,它展現(xiàn)了一種完全不同的編程思維。在本章筆者也有一些對問題的思考:
去除了副作用之后,所有問題的都有一套函數(shù)式的編程方案嘛?
筆者認為,Scala在意的是,如何進行函數(shù)式編程,并非所有問題的最佳方案都是使用函數(shù)式編程范式解決,函數(shù)式范式有自己的適用場景。
引用:
https://www.zhihu.com/question/28292740
《Scala函數(shù)式編程》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容