函數式非凡的抽象能力

我在閱讀或編寫具備函數式風格的代碼時,經常爲函數式思想非凡的抽象能力所驚歎。做爲一直以來持有OO信仰的程序員而言,對於「抽象」並不陌生。我甚至將面向對象思想的精髓定義爲兩個單詞:職責(Responsibility)與抽象(Abstraction)。只要職責分配合理,設計就是良好的;若能再加上合理的抽象,程序會變得更精簡且可擴展。若是你熟悉GoF的設計模式,你幾乎能夠從每一個模式中讀出「抽象」的意義來。javascript

然而,不管如何,面向對象思想構築的實際上是一個名詞的世界,這在很大程度上侷限了它的世界觀,它只能以實體(Entity)爲核心。雖然咱們仍然能夠針對實體提煉共同特徵,但這些特徵若爲行爲,卻沒法單獨存在,這是面向對象思想的硬傷。java

若是說面向對象思想是物質世界的哲學觀,則函數式思想展示的就是純粹的數學思惟了。函數做爲一等公民,它不表明任何物質(對象),而僅僅表明一種轉換行爲。是的,任何一個函數均可以視爲一種「轉換(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類型,就能夠抽象出以下的「轉換」操做:api

(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函數的封裝。相對而言,我更喜歡高階函數的形式:post

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函數中:url

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函數的執行結果:

lift的執行結果

諸如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的演講中給出了很是形象的對比:

OO與FP的設計原則

顯然,函數纔是最爲純粹的抽象。正所謂「大道至簡」,有時候,簡單可能就意味着一切。

相關文章
相關標籤/搜索