本文由 Shaw 發表在 ScalaCool 團隊博客。javascript
在用 Scala
作業務開發的時候,咱們大都會用到 case class
以及「模式匹配」,本文將介紹在平常開發中如何利用 case class
模擬 ADT
去良好地組織業務。java
在計算機編程、特別是函數式編程與類型理論中,
ADT
是一種composite type
(組合類型)。例如,一個類型由其它類型組合而成。兩個常見的代數類型是product
(積)類型 (好比tuples
和records
)和sum
(和)類型,它也被稱爲tagged unions
或variant type
。git
這裏簡單介紹一下常見的兩種代數類型 product
(積)類型和 sum
(和)類型github
在介紹兩種常見代數類型以前咱們先介紹一下 「計數」 的概念,方面理解後面所要介紹的內容。編程
爲了將某個類型與咱們熟悉的數字代數相關聯,咱們能夠計算該類型有多少種取值,例如 Haskell
中的Bool
類型:數據結構
data Bool = true | false複製代碼
能夠看到 Bool
類型有兩種可能的取值,要麼是 false
, 要麼是 true
, 因此這裏咱們暫時將數字 2
與 Bool
類型相關聯。函數式編程
若是 Bool
類型關聯的是 2
,那麼何種類型是 1
呢,在 Scala
中 Unit
類型只有一種取值:函數
scala> val a = ()
a: Unit = ()複製代碼
因此這裏咱們將數字 1
與 Unit
類型相關聯。fetch
有了 「計數」 這個概念,接下來咱們介紹常見的兩種代數類型。spa
product
能夠理解爲是一種 組合(combination
),能夠經過咱們熟悉的 *
(乘法) 操做來產生,對應的類型爲:
data Mul a b = Mul a b複製代碼
也就是說, a * b
類型是同時持有 a
和 b
的容器。
在 Scala
中,tuples
(元組)就是這樣的,例如:
scala> val b = (Boolean, Boolean)
b: (Boolean.type, Boolean.type) = (object scala.Boolean,object scala.Boolean)複製代碼
咱們定義的元組 b
就是兩個 Boolean
類型的組合,也就是說,元組 b
是同時擁有兩個 Boolean
類型的容器,能夠經過咱們前面介紹的 「計數」 的概念來理解:
Boolean
類型有兩種取值,當 Boolean
和 Boolean
經過 *
操做進行組合時:
2 * 2 = 4複製代碼
因此咱們定義的元組 b
有四種可能的取值,咱們利用 「模式匹配」 來列舉這四種取值:
b match {
case (true, true) => ???
case (true, false) => ???
case (false, true) => ???
case (false, false) => ???
}複製代碼
sum
能夠理解爲是一種 alternation
(選擇),能夠經過咱們熟悉的 +
操做來產生,對應的類型爲:
data Add a b = AddL a | AddR b複製代碼
a + b
是一個和類型,同時擁有 a
或者 b
。
注意這裏是 a
或者 b
,不一樣於上面介紹的 *
。
這裏可能就會有疑惑了,爲何 +
操做對應的語義是「或者」 呢,咱們依然經過前面介紹的 「計數」 的概念來理解:
在 Scala
中 Option
就是一種 sum
類型,例如:
scala> val c = Option(false)
c: Option[Boolean] = Some(false)複製代碼
option[Boolean]
實際上是 Boolean
與 None
經過 +
操做獲得的,分析:
Boolean
有兩種取值,None
只有一種,那麼:
2 + 1 = 3複製代碼
因此咱們定義的 c: Option[Boolean]
有三種可能的取值,咱們利用 「模式匹配」 來列舉這三種取值:
c match {
case Some(true) => ???
case Some(false) => ???
case None => ???
}複製代碼
咱們能夠看到,Option[Boolean]
類型的取值要麼是 Boolean
類型,要麼是 None
類型,這兩種類型是「不能同時」存在的,這一點與 product
類型不一樣。而且 sum
類型是一個「閉環」,類型的定義已經包含了全部可能性,絕無可能會出現非法狀態。
咱們在利用 Scala
的 case class
組織業務的時候其實就已經用到了 ADT
,例如:
sealed trait Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree複製代碼
在上面 「樹」 結構的定義中,Node
、Leaf
經過繼承 Tree
,經過這種繼承關係而獲得的類型就是 ADT
中的 sum
,而構造 Node
和 Leaf
的時候則是 ADT
中的 product
。你們能夠經過咱們前面所說的 「計數」的概念來驗證。
上面的代碼中出現了一個關鍵字 sealed
,咱們先介紹一下這個關鍵字。
前面咱們說過 sum
類型是一個 「閉環」,當咱們將「樣例類」的「超類」聲明爲 sealed
後,該超類就變成了一個 「密封類」,「密封類」的子類都必須在與該密封類相同的文件中定義,從而達到了上面說的「閉環」的效果。
好比咱們如今要爲上面的 Tree
添加一個 EmptyLeaf
:
case object EmptyLeaf extends Tree複製代碼
那這段被添加的代碼必須放在咱們上面聲明 Tree
的那個文件裏面,不然會報錯。
另外,sealed
關鍵字也可讓「編譯器」檢查「模式」語句的完整性,例如:
sealed trait Answer
case object Yes extends Answer
case object No extends Answer
val x: Answer = Yes
x match {
case Yes => println("Yes")
}
<console>: warning: match may not be exhaustive.
It would fail on the following input: No
x match {
^複製代碼
「編譯器」會在編譯階段提早給咱們一個可能會出錯的「警告(warning)」
前面說了這麼多,終於進入正題了,接下來咱們以幾個例子來講明如何在開發中合理地利用 ADT
。
如今咱們要開發一個與「優惠券」有關的業務,通常狀況下,咱們可能會這麼去定義優惠券的結構:
case class Coupon ( id: Long, baseInfo: BaseInfo, `type`: String, ... )
object Coupon {
//優惠券類型
object Type {
// 現金券
final val CashType = "CASH"
//折扣券
final val DiscountType = "DISCOUNT"
// 禮品券
final val GiftType = "GIFT"
}
}複製代碼
分析:這樣去定義 「優惠券」 的結構也能解決問題,可是當 「優惠券」 類型增多的時候,會出現不少的冗餘數據。好比說,不一樣的優惠類型,會有不一樣優惠信息,這些優惠信息在結構中對應的字段也會有所不一樣:
case class Coupon ( id: Long, baseInfo: BaseInfo, `type`: String, // 僅在優惠券類型是代金券的時候使用 leastCost: Option[Long], reduceCost: Option[Long], //僅在優惠券類型是折扣券的時候使用 discount: Option[Int], //僅在優惠券是禮品券的時候使用 gift: Option[String] )複製代碼
從上定義的結構咱們能夠看到,當咱們使用 「禮品券」 的時候,有三個字段(leastCost
、reduceCost
、discount
)的值是 None
,由於咱們根本就用不到。由此能夠看出,當 「優惠券」 的結構比較複雜的時候,可能會產生大量的冗餘字段,從而使咱們的代碼看上去很是臃腫,同時增長了咱們的開發難度。
ADT
從新組織:分析:經過上面的討論,咱們知道 「優惠券」 可能有多種類型,因此,咱們利用 ADT
將不一樣的「優惠券」分離開來:
// 將每一種優惠券公共的部分抽離出來
sealed trait Coupon {
val id: Long
val baseInfo: BaseInfo
val status: Int
val `type`: String
...
}
case class CashCoupon ( id: Long, baseInfo: BaseInfo, `type`: String = Coupon.Type.CashType, status: Int, leastCost: Long, reduceCost: Long, ... ) extends Coupon
case class DiscountCoupon ( id: Long, baseInfo: BaseInfo, `type`: String = Coupon.Type.DiscountType, status: Int, discount: Int, ... ) extends Coupon
case class GiftCoupon ( id: Long, baseInfo: BaseInfo, `type`: String = Coupon.Type.GiftType, status: Int, gift: String, ... ) extends Coupon複製代碼
同過合理地利用 ADT
咱們使每一種「優惠券」的結構更加清晰,同時也減小了字段的冗餘。而且,若是在業務後期咱們還要增長別的 「優惠券」類型,咱們不用修改原來的結構,只須要再從新建立一個新的 case class
就能夠了:
好比咱們在後期增長了一種叫 「團購券」 的優惠券,咱們不須要修改原來定義的結構,直接:
case class GroupCoupon ( id: Long, baseInfo: BaseInfo, `type`: String, status: Int, dealDetail: String )複製代碼
而且在利用「模式匹配」的時候,咱們能夠像操做代數那樣:
coupon match {
case c: CashCoupon => ??? // 咱們能夠直接在匹配完成以後使用 coupon
case c: DiscountCoupon => ???
case c: GiftCoupon => ???
case c: GroupCoupon => ???
}
// 若是是咱們用 ADT 改造前的數據結構,那模式匹配就會變成:
coupon.`type` match {
case Coupon.Type.CashType => ??? // 咱們只能使用 coupon.`type`
case Coupon.Type.GiftType => ???
case Coupon.Type.DiscountType => ???
case Coupon.Type.GroupCoupon => ???
}複製代碼
經過本例,咱們能夠看到,利用 ADT
從新組織以後的數據結構減小了數據的冗餘,而且在使用「模式匹配」的時候更加清晰,在功能上也更增強大。
針對上面的優惠券,用戶在使用這些優惠券的時候,優惠券會存在不一樣的幾種狀態:
未領取
已領取但暫未使用
已使用
過時優惠券
無效優惠券
咱們如今想要根據這幾種不一樣的狀態渲染出不一樣的結果頁面,要獲得這幾種狀態,咱們一般會:
def fetched(c: Coupon, user: User) = {
//根據coupon信息以及user信息去查詢用戶是否已經領取了這張優惠券
???
}
def used(c: Coupon, user: User) = {
//根據coupon信息以及user信息去查詢用戶是否已經使用了這張優惠券
???
}
def isExpired(c: Coupon) = {
//根據優惠券信息來判斷優惠券是否已通過期
???
}
def isAviable(c: Coupon) = {
//根據優惠券信息來判斷優惠券是否已經失效
???
}複製代碼
咱們如今就利用這些狀態去渲染頁面:
def f(c: Coupon, user: User) = {
if (!isAviable(coupon)) {
if (!isExpired(coupon)) {
if (used(coupon, user)) {
//已使用的優惠券
???
} else {
if (fetched(coupon, user)) {
//已領取但未使用的優惠券
???
} else {
//未領取的優惠券
???
}
}
} else {
//已過時的優惠券
???
}
} else {
//已失效的優惠券
???
}
}複製代碼
上面的代碼可以完成咱們的需求,可是,當優惠券的狀態變多的時候,該方法傳入的參數也會有所變化,「if-else」語句層級也會越多,很是容易出錯,同時代碼錶達的意思也沒那麼明確,可讀性極差。
因此咱們可否從新組織一下數據結構,使之可以利用「模式匹配」?
ADT
從新組織:分析:咱們在使用優惠券的時候無非就是判斷這幾種「狀態」,那咱們就利用 ADT
將這些狀態抽象化:
sealed trait CouponStatus {
//每種狀態共用的一些信息
val base: CouponStatusBase
}
case class CouponStatusBase ( coupon: Coupon, ... )
//未領取
case class StatusNotFetched ( base: CouponStatusBase ) extends CouponStatus
//已領取但未使用
case class StatusFetched ( base: CouponStatusBase, user: User ) extends CouponStatus
//已使用
case class StatusUsed ( base: CouponStatusBase, user: User ) extends CouponStatus
//過時優惠券
case class StatusExpired ( base: CouponStatusBase ) extends CouponStatus
case object StatusUnAvilable extends CouponStatus複製代碼
咱們利用 ADT
將「狀態」抽象化了,而且將每種「狀態」所須要使用到的數據所有構造在了一塊兒,那如今咱們再根據不一樣的「狀態」去渲染頁面就變成了:
def f(status: CouponStatus) = status match {
case StatusNotFetched(base) => ???
case StatusFetched(base, user) => ???
case StatusUsed(base, user) => ???
case StatusExpired(base) => ???
case StatusUnAvilable => ???
}複製代碼
能夠看到經過用 ADT
抽象以後的數據結構在「模式匹配」的時候很是清晰,而且咱們將不一樣狀態下所須要的數據所有構造在了一塊兒,也使得咱們在模式匹配以後能夠直接利用 status
去使用這些數據,不用再經過方法去獲取了。
經過本例,咱們能夠發現,經過 ADT
能夠將數據「高度抽象」,使得數據的「具體信息」變得簡潔,同時「歸納能力」變得更強,數據更加「完備」。
The Algebra of Algebraic Data Types, Part 1