我在閱讀或編寫具有函數式風格的代碼時,常常為函數式思想非凡的抽象能力所驚嘆。作為一直以來持有OO信仰的程序員而言,對於“抽象”並不陌生。我甚至將面向對象思想的精髓定義為兩個單詞:職責(Responsibility)與抽象(Abstraction)。只要職責分配合理,設計就是良好的;若能再加上合理的抽象,程序會變得更精簡且可擴展。如果你熟悉GoF的設計模式,你幾乎可以從每個模式中讀出“抽象”的意義來。
然而,無論如何,面向對象思想構築的其實是一個名詞的世界,這在很大程度上局限了它的世界觀,它只能以實體(Entity)為核心。雖然我們仍然可以針對實體提煉共同特征,但這些特征若為行為,卻無法單獨存在,這是面向對象思想的硬傷。
如果說面向對象思想是物質世界的哲學觀,則函數式思想展現的就是純粹的數學思維了。函數作為一等公民,它不代表任何物質(對象),而僅僅代表一種轉換行為。是的,任何一個函數都可以視為一種“轉換(transform)”。這是對行為的最高抽象,代表了類型(type)[注意,是類型(type),而不是類(class)]之間的某種動作。函數可以是極為原子的操作,也可以是多個原子函數的組合,或者在組合之上再封裝一層語義更清晰的函數表現。
理解了函數的轉換本質,我們就必須學會在具體行為中“洞見”這種轉換本質。這種“洞見”可以理解為解構分析,就好似我們在甄別化石的年代時,利用核分析技術去計算碳14同位素原子數量一般。我們解構出來的“原子”函數往往具有非凡的抽象能力。例如,我們針對集合的sum與product操作,可以解構出原子的fold函數。雖然從行為特征看,sum為求和,product為求積,但從抽象層面看,都是從一個初始值開始,依次對集合元素進行運算。而運算本身,又是抽象的另一個轉換操作,從而引入了高階函數的概念。若要讓fold不止局限於某一種具體類型,則可以引入函數式語言的類型系統。fold可以根據折疊的方向分為foldRight與foldLeft。foldRight(或flodr)的函數定義如下:
//scala語言
def fold[A, B](l: MyList[A], z: B)(f: (A, B) => B):B = l match {
case Nil => z
case Cons(x, xs) => f(x, fold(xs, z)(f))
}
--haskell語言
foldr f zero (x:xs) = f x (foldr f zero xs)
foldr _ zero [] = zero
《深入理解Scala》一書在講解Scala的Option時,給出了一個有趣的案例,其中揭示的抽象思想與fold有異曲同工之妙。這個案例講解了如何用多個可能未初始化的變量構造另一個變量,Option正適合處理這種情況,我在博客《並非Null Object這么簡單》中介紹了Option的本質,這里不再贅述。這個例子是希望通過數據庫配置信息創建連接。由於配置信息可能有誤,創建的連接可能為null,因而使用Option的api會更加健壯:
def createConnection(conn_url: Option[String],
conn_user: Option[String],
conn_pw: Option[String]): Option[Connection] =
for {
url <- conn_url
user <- conn_user
pw <- conn_pw
} yield DriverManager.getConnection(url, user, pw)
現在,我們將這個函數無限抽象化,那就是要去掉一些復雜而冗余的具象信息,就好像過濾掉讓人眼花繚亂的繽紛顏色,僅僅呈現最朴素的黑白二色一般。首先,我們抹掉“創建連接”的特征,然后再抹掉類型信息。我們可以看到createConnection實則是對DriverManager.getConnection的轉換,經此轉換后,若要創建連接,就可以傳入三個Option[String]類型的參數,獲得Option[Connection]類型的結果。然后再去掉具體的String類型,就可以抽象出如下的“轉換”操作:
(A, B, C): => D 轉換為 (Option[A], Option[B], Option[C]) => Option[D]
注意,這個轉換操作是函數到函數的轉換。
書中找到了一個正確的概念來恰如其分地描述這一“轉換”操作,即為lift(提升):
def lift[A, B, C, D](f: Function3[A, B, C, D]): Function3[Option[A], Option[B], Option[C], Option[D]] =
(oa: Option[A], ob: Option[B], oc: Option[C]) =>
for (a <- oa; b <- ob; c <- oc) yield f(a, b, c)
Function3事實上是Scala中對(A, B, C) => D函數的封裝。相對而言,我更喜歡高階函數的形式:
def lift[A, B, C, D](f: (A, B, C) => D): (Option[A], Option[B], Option[C]) => Option[D] =
(oa: Option[A], ob: Option[B], oc: Option[C]) =>
for (a <- oa; b <- ob; c <- oc) yield f(a, b, c)
lift函數是寬泛的抽象,之前的DriverManager.getConnection()函數則為一個具體的被轉換對象。它可以作為參數傳入到lift函數中:
val createConnection1 = lift(DriverManager.getConnection)
lift函數返回的實則是一個函數,它本質上等同於之前定義的createConnection()函數。由於lift抹掉了具體的類型信息,使得它不僅僅可以將getConnection提升為具有Option的函數,還能針對所有形如(A, B, C) => D格式的函數。讓我們來自定義一個combine函數:
def combine(prefix: String, number: Int, suffix: String): String =
s"$prefix - $number - $suffix"
val optionCombine = lift(combine)
區分combine函數與opitonCombine函數的執行結果:
諸如fold或lift這樣的終極抽象在函數式語言的api中可謂俯拾皆是,如針對集合的monad操作filter, flatMap, map,又例如函數組合的操作sequence,andThen等。我們還可以結合轉換語義為這種基本轉換命名,使得代碼更加簡略可讀。例如針對如下的三個函數定義:
def intDouble(rng: RNG): ((Int,Double), RNG)
def doubleInt(rng: RNG): ((Double,Int), RNG)
def double3(rng: RNG): ((Double,Double,Double), RNG)
我們可以抽象出RNG => (A, RNG)的通用模式,然后從語義上將其命名為Rand,那么,在scala中可以利用type關鍵字為這種轉換定義別名:
type Rand[+A] = RNG => (A, RNG)
當我們將函數作為基本的抽象單元后,再對面向對象思想做一次回眸,會發現OO中的多數設計原則與設計模式,都可以簡化為函數。Scott Wlaschin在Functional Design Patterns的演講中給出了非常形象的對比:
顯然,函數才是最為純粹的抽象。正所謂“大道至簡”,有時候,簡單可能就意味着一切。