本文由 Yison 發表在 ScalaCool 團隊博客。html
若是你看到一個開源類庫,logo 是四隻貓 + 五個箭頭,可能會略感不適 — 這是工程代碼裏可使用的玩意兒嗎?java
沒錯,這是 TypeLevel 設計的一個函數式類庫,一羣推崇函數式編程的傢伙注入到 Scala 生態中的重磅炸彈,它是 Cats,源於 Category(範疇)的縮寫。git
在 水滴 的系統中,咱們大規模使用了 Cats 來解決一些業務問題,而且從中受益。但顯然這並非 Scala 標準庫所支持的打法,因此本系列旨在系統地介紹這個類庫,讓更多人瞭解它。github
咱們最開始使用的是 Scalaz,它是 Cats 的前身,因爲語法問題致使不少吐槽,以後採用 Cats 替代。數據庫
固然,不少了解過 Cats 的人知道,關於它已經有如下這些優秀的學習資料:編程
但顯然,若是以前並無函數式編程經驗的同窗,可能會在首次閱讀這些資料的時候犯困。所以,該系列但願在正式介紹 Cats 這個神器以前,先友好地探討一些關於「函數式編程」的基本問題。如:數據結構
那麼,什麼纔是函數式編程呢?其實,關於這點並無準確權威的定義,本文也不想就此給出一個答案。異步
可是,咱們但願來討論下什麼是「函數式編程思惟」,它跟咱們熟知的「命令式編程」到底有哪些不一樣。async
常常上知乎的朋友發現了,這是知乎上一個很是好的問題。編程語言
本文推薦如下的回答:
@nameoverflow: 函數式編程關心數據的映射,命令式編程關心解決問題的步驟。
更數學化的版本: @parker liu 函數式編程關心類型(代數結構)之間的關係,命令式編程關心解決問題的步驟。
函數式編程的思惟就是如何將類型(代數結構)之間的關係組合起來,用數學的構造主義構造出你設計的程序。
咱們來解剖這個結論,直觀上函數式編程是一件很是簡單的事情,咱們只需作一件事情就夠了,那就是「組合」。
但此時,咱們確定又變得一頭霧水,如下問題緊接着就來了:
別急,咱們先來討論一個基本的問題 — 什麼是過程和數據?
看過 SICP 的朋友確定已經發現,這是這本書第一章和第二章所研究的內容。
簡單來講,數據是一種咱們但願去操做的東西,而過程就是有關操做這些數據的規則的描述。它們構成了程序設計的基本元素。
在 Lisp 這種函數式編程語言中,過程和數據同樣,屬於第一級狀態,這也就意味着:
舉個例子,咱們能夠定義一個過程,它接受的參數是一個過程,返回的是另一個過程,這彷佛看起來有點怪。 換個例子,定義一個過程,它接受的參數是一個數,返回的是另一個數,這是否是就熟悉多了?
在函數式編程中,咱們會發現其實「過程」和「數據」的界限有時候是模糊的。也就是說,有時咱們能夠把它們看成一個東西。
回到咱們剛纔的結論:「函數式編程關心類型(代數結構)之間的關係」。
咱們因而能夠這麼理解,函數式編程要解決的第一個問題:就是須要足夠高的抽象能力,能對各類數據和過程進行抽象,提供類型(代數結構)。
這也一樣是後續咱們在學習 Cats 過程當中要得到解答的一個疑問,它是如何幫助咱們實現這一點。
推薦閱讀 @shaw 寫的 如何在 Scala 中利用 ADT 良好地組織業務
其次,咱們再來討論下,到底什麼是所謂的「關係」?
咱們確定有這樣子的疑惑,若是函數式編程思惟僅靠「組合」就可以描述全部的程序,那麼在理論上它必須具有完備性。
很多朋友知道所謂的 [圖靈完備](Turing completeness)。它聽起來逼格很高,其實這是一個很簡單的概念,意味着用圖靈機能作到的全部事情,能夠解決全部的可計算問題。
另一個可支持解決全部可計算問題的方案就是「Lambda 演算」。在 Lisp 這種函數式編程語言中的基石,就是這個理論。
函數式編程中的 lambda 能夠當作是兩個類型之間的關係,一個輸入類型和一個輸出類型。lambda 演算就是給 lambda 表達式一個輸入類型的值,則能夠獲得一個輸出類型的值,這是一個計算,計算過程知足 \alpha -等價和 \beta -規約。
關於圖靈完備和 Lambda 演算,有機會咱們能夠在後續的文章中繼續討論。
咱們再來聊聊核心,所謂的「組合」。
「面向組合子編程」是十年前 javaeye 的牛人 @Ajoo 提出的概念。
首先,咱們能夠引用哲學的基本方法來比喻咱們常見的「面向對象編程」與「面向組合子編程」差別。
前者是概括法,後者是演繹法。
也就說,咱們在用 Java 這些面向對象的語言進行程序設計的時候,一般採用的是總結的方法,然而函數式編程語言提倡的「組合」,更貼近數學的思惟,它是一種推導。
因此,函數式編程所關心的組合,更多作的是先高度抽象類型關係,而後對這些關係的轉化,所謂的 transformer。
因而,咱們得出第二個關鍵的問題:即 Cats 如何提供足夠的 transformer,來幫助咱們實現各類關係之間的組合。
對於第一次接觸這些概念的朋友來講,仍是有點抽象,下面咱們來舉一個實際的例子來加深認識。
假設咱們如今要設計一個抽獎活動的參與過程,涉及如下邏輯:
import org.joda.time.DateTime
import scala.concurrent.Future
case class Activity(id: Long, start: DateTime, end: DateTime)
case class Prize(id: Long, name: String, count: Int)
val activity = syncGetActivity()
val prizes = syncGetPrizes(activity.id)
if (activity.start.isBefore(DateTime.now())) {
println("activity not starts")
} else if (activity.end.isBefore(DateTime.now())) {
println("activity ends")
} else if (prizes.map(_.count).sum < 1) {
println("activity has no prizes")
} else {
println("activity is running")
}
複製代碼
import org.joda.time.DateTime
import scala.concurrent.Future
case class Activity(id: Long, start: DateTime, end: DateTime)
case class Prize(id: Long, name: String, count: Int)
sealed trait ActivityStatus {
val activity: Activity
val prizes: Seq[Prize]
}
case class ActivityNotStarts(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus
case class ActivityEnds(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus
case class ActivityPrizeEmpty(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus
case class ActivityRunning(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus
def getActivityStatus(): Future[ActivityStatus] = {
for {
activity <- asyncGetActivity()
prizes <- asyncGetPrizes(activity.id)
} yield (activity, prizes) match {
case (a, pzs) if a.start.isBefore(DateTime.now()) => ActivityNotStarts(a, pzs)
case (a, pzs) if a.end.isBefore(DateTime.now()) => ActivityNotStarts(a, pzs)
case (a, pzs) if pzs.map(_.count).sum < 1 => ActivityPrizeEmpty(a, pzs)
case (a, pzs) => ActivityRunning(a, pzs)
}
}
複製代碼
以上,咱們能夠發現函數式風格,會傾向於基於更高的業務層次進行抽象,直覺上是一個 describe what 的設計,而不是 how to do。
值得一提的是,asyncGetActivity
這個從數據庫異步獲取活動數據過程,它的類型是一個高階類型 Future[Activity]
,這也就是咱們以前提到的對過程進行抽象,定義類型。
經過對 asyncGetActivity
和 asyncGetPrizes
兩個異步過程的組合,咱們最終轉化獲得了 ActivityStatus
這個類型的對象結果。
Scala 是一門結合「面向對象」和「函數式」的編程語言,咱們用它能夠寫出大相徑庭的代碼風格。不少人把它看成 better Java 來使用,但若是結合 Cats 這個函數式類庫,咱們就能夠更好地採用函數式編程思惟來設計程序,從而發揮 Scala 更大的威力。
經過該篇文章,咱們對函數式編程有了直覺上的感覺。固然,你可能依舊雲裏霧裏,沒關係,咱們會在後續的文章裏進一步的討論。在下一篇文章中,咱們會介紹下函數式編程所帶來的優點。