初探函數式編程的優點

初探函數式編程的優點

函數式編程是一種編程的模式,在這種編程模式中最常用的函數和表達式。它強調在編程的時候用函數的方式思考問題,函數也與其他數據類型一樣,處於平等地位。可以將函數作爲參數傳入另一個函數,也可以作爲別的函數的返回值。函數式編程傾向於用一系列嵌套的函數來描述運算過程。

函數式編程的優點

不可變數據

原則上來講,在函數式編程中應當做到函數中變量不修改,函數不依賴外部數據,所有的變量需要修改時都應當將原變量作爲參數傳入到函數中,並在函數中通過運算返回一個新的變量。
例如下面代碼:


int sum;
int increment(int a){
    sum = sum + a;
    return sum;
}

如果按照函數式編程方法則應當寫成:


def increment(sum: Int, a: Int) = {
    sum + a
}

代碼本身沒有什麼問題,但是使用下面的表達方式會解決第一種方式的三個問題。第一,上面的increment方法中依賴了成員變量sum,因此每次調用該方法返回的值根據成員變量的狀態會發生變化,這在代碼出現問題debug的時候會很難排查問題,在編寫單元測試的時候也需要注意成員變量的狀態。第二,第一種寫法的increment方法其實做了兩件事,即修改成員變量的值和增加後返回成員變量。調用者在調用這個方法的時候很可能只爲了獲取增加後的數值,並沒有意識到會改變成員變量,也會後面的代碼埋下了隱患。第三,在併發的情況下第一種方法的結果更難以預測,而使用函數式編程由於沒有改變任何變量,因此不存在併發問題。

強制使用遞歸:

在函數式編程中,函數是沒有狀態的,不存在可變變量的,那麼如果真的在程序中需要用到狀態的時候應當怎麼辦呢?舉例來說如果要求x的n次方,我們可能會寫如下代碼。


long power(int x, int p){
    long result = 1;
    for(int i = 0; i < p; i++)
        result = result * x;
    return result;
}

其中用result記錄了每次計算後得到的值。那麼如果使用函數式編程應當如何實現這個函數呢?答案是將每次的狀態都作爲參數傳遞到函數中去,說白了就是遞歸。


def power(x: Int, p: Int): Long = {
    if(p == 0)
        return 1
    else
        return x * power(x, p - 1)
}

遞歸對於函數式編程來說是非常重要和常用的,在利用函數式編程解決問題的很多時候遞歸不是可選而是必選的方式。這也一定程度上使得代碼更具有可讀性,當然,可讀性的問題仁者見仁智者見智,對每個人來說習慣的就是可讀的,但是我認爲遞歸和循環最本質的區別在於,循環是思考如何解決這個問題,而遞歸是思考如何描述這個問題。從這個角度來看,無論是編寫還是閱讀,遞歸都有着一定的優勢。
很多同學可能會質疑,從性能上來說,遞歸有着難以避免的佔用堆棧空間問題。不過函數式編程的尾遞歸優化可以一定程度上解決這個問題。在前面討論遞歸的時候我們說過,遞歸可以將需要保存的狀態變量通過參數方式傳遞給下一層的函數,也就是說如果遞歸發生在函數最後一行的話,那函數前面的所有內容其實都不需要存儲,遞歸的函數也就可以重新利用當前的棧空間,這樣以來遞歸函數所需要的佔空間就是常數階的了。

高階函數和閉包:

函數式編程中的高階函數和閉包能夠減少代碼的重複率,讓代碼更具有模塊化。比如說需要實現三個字符串過濾器,分別完成根據字符串尾過濾,根據字符串包含過濾以及根據正則式過濾。我們可能會寫下這樣的代碼。


public List strEnding(List list, String query){
        List result = new ArrayList();
        for (String str : list) {
            if(str.endsWith(query))
                result.add(str);
        }
        return result;
    }
    public List strContaining(List list, String query){
        List result = new ArrayList();
        for (String str : list) {
            if(str.contains(query))
                result.add(str);
        }
        return result;
    }
    public List strRegex(List list, String query){
        List result = new ArrayList();
        for (String str : list) {
            if(str.matches(query))
                result.add(str);
        }
        return result;
    }

如果使用高階函數則可以將過濾函數作爲參數傳遞到函數中,即


def strMatching(list: List[String], query: String, matcher:(String, String) => Boolean) = {
        for(str <- list; if(matcher(str, query))) yield str
    }
def strEnding(list: List[String], query: String) = strMatching(list, query, _.endsWith(_))
def strContaining(list: List[String], query: String) = strMatching(list, query, _.contains(_))
def strRegex(list: List[String], query: String) = strMatching(list, query, _.matches(_))

這樣一來就提高了代碼的重用率。
以上是僅僅是本人初學scala的一些對於目前已有的一些觀點的個人理解(基本上都是拾人牙慧和自己瞎扯)。一定有許多錯誤和理解不準確的地方,希望能夠得到各位的批評指正和指點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章