從函數語言程式設計說起

akiyama發表於2019-02-28

前言

“好事者”總結了一個份關於Android開源專案的榜單,榜單裡面包含了Android開發中常用的開源庫,排在第一的是網路封裝框架Retrofit,而RxJava(RxAndroid)則排名十三。榜單是以使用優先順序來評判的,Android開發中必不可少的模組排名會高一些(使用多而選擇少,所以人人都會去用),例如網路載入框架(Retrofit+okhttp),圖片載入框架(glide)等。

但是如果讓我選擇一個對程式開發模式或者整個App架構影響最大的開源庫,我會毫不猶豫的選擇RxJava;如果把開發App比喻成做飯,Retrofit或者okhttp充其量是蔥、蒜、姜,而RxJava則是主材;蔥、蒜、姜能夠影響菜的口味,但是最終做出來菜是怎樣則是由主材決定的。

榜單中把RxJava的關鍵字定義成“非同步”,這是錯誤或者說是膚淺的。RxJava本質是一種函數語言程式設計的實現框架,它從根本上改變了Android開發過程中的思維方式,下面來看看什麼是函數語言程式設計。

函數語言程式設計是什麼?

要釐清函數語言程式設計,我們必須先明確幾個概念的區別:

  • 物件導向程式設計程式導向程式設計(程式設計思想)
  • 函數語言程式設計指令式程式設計(程式設計正規化)

前者是程式設計思想,後者屬於程式設計正規化,一般來說前者包含後者。首先需要明確一點:這四個概念不是對立的,他們之間都是互相關聯和重疊的;程式設計思想並不是非此即彼,物件導向也有可能會用到程式導向的思想,程式導向也有可能會用到物件導向的思想。函數語言程式設計和指令式程式設計也一樣,區別在於是否是大部分的思維方式契合某種程式設計方式。

函數語言程式設計很早的時候就已經出現了,像古老的Lisp語言就是函數語言程式設計的典範,最近的火熱的kotlin語言也屬於函數語言程式設計語言,看一個kotlin的函式程式設計例子:

 /**
  * 找出list中某些單詞的使用頻率,從高到底輸出
  */
private fun test(list : MutableList<String>):TreeMap<String, Int>{
        val words = mutableSetOf("the","abd","to")
        val wordMap = TreeMap<String, Int>()
        list.stream()
                .map { w->w.toLowerCase() }
                .filter { w->!words.contains(w) }
                .forEach{w->wordMap.put(w,wordMap.getOrDefault(w,0)+1}
        return wordMap;
    }

複製程式碼

相比於傳統的Java語言程式設計方式,函數語言程式設計更加註重“演算法”,回憶大學學習演算法課程的時候,老師教學過程中的往往是使用虛擬碼,比如找出A B C三個數中最大值。

Begin(演算法開始)
輸入 A,B,C
IF A>B 則 A→Max
否則 B→Max
IF C>Max 則 C→Max
Print Max
End (演算法結束)
複製程式碼

而函數語言程式設計語言可能相比更加精煉一些,

Begin(演算法開始)
輸入 A,B,C
max -> Max(A,B)
max->Max(max,c)
Print max
End (演算法結束)

複製程式碼

函數語言程式設計語言更加關注函式,函式在程式設計過程中是“一等公民”,上面例子中,就像虛擬碼一樣,程式碼把整個問題域分解成粒度合適的小問題,通過解決一個個小問題,從而實現整個問題的求解,a->fun1->fun2->b。

在指令式程式設計過程中我們需要對每一個小問題進行程式碼封裝求解,更加關注“問題域”的解決思路,忽略實現的細節。物件導向程式設計其實也是包含這種思想,花更多的時間關注更高層次問題的抽象,考慮解決複雜的業務場景。

在《函數語言程式設計思維》書上有一句原文:“假如語言不對外暴露那麼多出錯的可能性,那麼開發者就不那麼容易犯錯”,對於這句話我的理解是:

  • 儘可能的對基礎函式做封裝,比如對基本資料結構的操作,讓呼叫者儘可能的具有確定性的輸入和輸出,比如上面例子中對集合的操作map、filter等等;
  • 儘可能讓可變的東西變成不可變,例如變數、多執行緒、集合等等;

前面這兩個結論就隱含著函數語言程式設計語言中巨大的設計思路,函數語言程式設計中函式指的是數學概念上的函式f(x),函式的值僅決定於函式引數的值,不依賴其他狀態,在純粹的函數語言程式設計語言中必須遵從兩個規範:

  • 不可變性
  • 對基礎函式(高階)封裝

為這兩條就是函數語言程式設計語言的本質特徵。

函數語言程式設計語言的不可變性

說到函數語言程式設計的不可變特性,我們必須先說一說指令式程式設計的“可變性”,在傳統的Java語言中,我們時刻需要儲存一些計算“狀態”資訊,這些可能是一些具體的資料、引用指向等等資訊,比如我們要求解0-10的和。

public void sum(){
    int sum = 0;
    for(int i=0;i<10;i++){
        sum += i;
    }
    return sum;
}
複製程式碼

需要不斷對sum賦值,sum這個變數一直在不斷的被重用,因此我們說sum是可變的。

這樣會帶來什麼後果?假設sum這個值是一個物件成員變數,如果該物件被多個執行緒呼叫,sum值就會處於一種執行緒不安全的狀態,假設sum一個引用型別的變數,經過多個函式的呼叫引用型別會不斷被賦值,最終會導致輸出的結果是不可預見的,總結不可變帶來的一些問題:

  • 執行緒不安全
  • 應用不透明
  • 資源被競爭
  • 可測性下降

那在Java中有沒有不可變特性的體現呢?String類就是最明顯的不可變的實現,String類的實現具體以下幾個特性:

  • 宣告成 final,包括某些關鍵的成員變數、類的宣告等等
  • 無引數的建構函式

String類的實現本質就是為了“安全的賦值”,當我們定義一個字串,並且重新賦值,它並非是在同一個記憶體引用上修改資料,而是重新生成一個新的引用

String a= "1122";
a = "3333";
複製程式碼
image

那麼在函數語言程式設計語言中是怎麼實現的“不變性”的呢,一般從以下幾個方面進行考慮:

  • 語法層面進行約束和規範。例如kotlin的val不可變變數,可變集合和不可變集合的規範等等;
  • 利用函式進行引數的傳遞。函式的輸入固定,輸出也是固定的,所以就保證了函式式引數的不變性;
  • 善用遞迴等語言特性;
  • 使用高階函式;

上面的求和例子,在kotlin函式式語言中,實現的方式:

fun sum() : Double{
        val a = 9 // val的不可變數
        // 高階函式進行資料求和
        return Array<Int>(a,{ w->w+1}).sumByDouble({w->w.toDouble()})
    }
複製程式碼

可以看到,kotlin貫徹執行了函式式語言的不可變特性,通過高階函式來對整個過程中產生的臨時變數全部轉換成函式引數,類似於f(g(x))。

函數語言程式設計語言之所以會流行起來很大原因是因為“不變性”,隨著摩爾定律在計算機上面的失效,導致計算機的效能提升更多依賴於多核計算,而多核計算的最大問題在於“併發”的處理,函式式語言通過避免賦值產生的“不變性”恰恰契合了多核計算機的發展。

lambda表示式和高階函式

lambda表示式在函數語言程式設計語言中往往體現在“匿名函式”上。當你需要一個函式,但是你又不想要給這個一次性函式取一個“不需要”的名字,這個時候匿名函式就可以派上用場了。在Android中匿名函式隨處可見,例如設定一個監聽:

 mCompleteBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                doSomeThink(v)
            }
        });

複製程式碼

在Java中這種對匿名函式的寫法是極其不優雅和麻煩的,通過lambda表示式去簡化這段程式碼,只保留引數和對應的方法實現:

 mCompleteBtn.setOnClickListener({v->doSomeThink(v)})
複製程式碼

我們把

{v->doSomeThink(v)}
複製程式碼

這段程式碼塊稱之為lambda表示式,很明顯lambda表示式最大的作用就是:

==讓程式碼更簡潔==

在多個引數的時候lambda表示式的寫法類似

(x,y)->x^2+y^2
複製程式碼

高階函式其實就是利用lambda表示式作為引數或者引數返回值的函式,例如:

 val items = listOf(1, 2, 3, 4, 5)
 items.map ({t->t*2})
複製程式碼

其中map就是高階函式,

 public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
複製程式碼

當然任何高階函式本質上都是指令式程式設計的封裝,函數語言程式設計會對一些常用的資料結構和集合(set map)做一些函式式封裝。

總結

程式設計正規化理論上是沒有所謂的好壞之分,只有合適與否。某種意義上來說,函數語言程式設計和指令式程式設計在底層底層實現並沒有本質上的區別,只不過函數語言程式設計通過一系列的規範來保證程式設計過程中儘可能的遵循“不可變性”,通過對基本資料結構和集合提供足夠多的高階函式,從而讓程式設計者更加關注解決問題的步驟而非具體的實現。

參考文章

  • https://www.ibm.com/developerworks/cn/java/j-ft20/index.html
  • http://www.ruanyifeng.com/blog/2012/04/functional_programming.html

相關文章