Cats(一):從函數式編程思惟談起

本文由 Yison 發表在 ScalaCool 團隊博客。html

Cats logo

若是你看到一個開源類庫,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 良好地組織業務

圖靈完備與 Lambda 演算

其次,咱們再來討論下,到底什麼是所謂的「關係」?

咱們確定有這樣子的疑惑,若是函數式編程思惟僅靠「組合」就可以描述全部的程序,那麼在理論上它必須具有完備性。

很多朋友知道所謂的 [圖靈完備](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],這也就是咱們以前提到的對過程進行抽象,定義類型。

經過對 asyncGetActivityasyncGetPrizes 兩個異步過程的組合,咱們最終轉化獲得了 ActivityStatus 這個類型的對象結果。

總結

Scala 是一門結合「面向對象」和「函數式」的編程語言,咱們用它能夠寫出大相徑庭的代碼風格。不少人把它看成 better Java 來使用,但若是結合 Cats 這個函數式類庫,咱們就能夠更好地採用函數式編程思惟來設計程序,從而發揮 Scala 更大的威力。

經過該篇文章,咱們對函數式編程有了直覺上的感覺。固然,你可能依舊雲裏霧裏,沒關係,咱們會在後續的文章裏進一步的討論。在下一篇文章中,咱們會介紹下函數式編程所帶來的優點。

相關文章
相關標籤/搜索