函數語言程式設計最佳實踐

夏天來嘍發表於2019-03-22

這篇文章不會大講什麼是函數語言程式設計,為什麼要使用函數語言程式設計,而是開門見山,介紹函數語言程式設計的最佳實踐,希望大家讀完了這篇文章,會理解函數語言程式設計的美妙並且愛上它。

1. 純函式

純函式是這樣一種函式:對於同樣的輸入,總是會產生同樣的輸出,沒有副作用, 儘量在你的程式碼中使用純函式,這會使你的程式碼更加健壯,測試更加容易。

接下來來一起看看各種副作用

1.1 引用自由變數

看下面這段程式碼

  function foo(x) {
    y = x * 2;
  }
  var y;
  foo( 3 );
複製程式碼

分析上面的程式碼,呼叫函式改變了函式外的變數y, 產生了副作用,因此不是純函式

當一個函式引用了函式外的變數,也就是自由變數,並不是所有的自由變數引用都是糟糕的,但是我們處理的時候必須非常小心

我們可以非常容易地將他變成純函式

function foo(x) {
    return x*2;
}
foo( 3 );
複製程式碼

1.2 隨機數

隨機數也會產生副作用,對於同樣的輸入,結果是無法預測的

1.3 IO

最常見的副作用是輸入輸出,一個程式沒有IO是完全沒有意義的,因為它的工作不能用任何方式觀測到,舉個最常見的例子

var users = {};

function fetchUserData(userId) {
    ajax( `http://some.api/user/${userId}`, function onUserData(user){
        users[userId] = user;
    } );
}
複製程式碼

fetchUserData改變了users, 想變得更純一點,我們可以建立一個包裹函式safer_fetchUserData,將外部變數和不純的函式都包裹起來

function safer_fetchUserData(userId,users) {
    // 拷貝一份外部變數
    users = Object.assign( {}, users );

    fetchUserData( userId );

    return users;


    // ***********************

    // 原始的非純函式:
    function fetchUserData(userId) {
        ajax(
            `http://some.api/user/${userId}`,
            function onUserData(user){
                users[userId] = user;
            }
        );
    }
}
複製程式碼

safer_fetchUserData更純一點,但是依然不是純函式,因為依賴ajax呼叫的結果,ajax的副作用是無法消除的 從上面也可以看出,副作用無法完全消除,我們只能儘可能地寫純函式,將不純的部分都收集在一起

2. 一元函式(單引數)

考慮下面這段程式碼

["1","2","3"].map( parseInt );
複製程式碼

相信很多人都會不假思索的回答[1,2,3], 但是真實的結果是[1, NaN, NaN], 認真思考一下array.map(fn)這個高階函式的執行過程,在每一輪的迭代中,fn函式都會執行,執行的時候會傳入三個引數, 分別是陣列的這一輪的元素,索引,和陣列本身,所以真實的執行情況是

parseInt("1", 0, ["1","2","3"])
parseInt("2", 1, ["1","2","3"])
parseInt("3", 2, ["1","2","3"])
複製程式碼

parseInt(x,radix)可以接受兩個引數,因為第三個引數忽略,第一個引數x代表要轉換的值, 第二個引數radix是轉換進位制,當引數 radix 的值為 0,或沒有設定該引數時,parseInt() 會根據 string 來判斷數字的基數。

當忽略引數 radix , JavaScript 預設數字的基數如下:

如果 string 以 "0x" 開頭,parseInt() 會把 string 的其餘部分解析為十六進位制的整數。

如果 string 以 0 開頭,那麼 ECMAScript v3 允許 parseInt() 的一個實現把其後的字元解析為八進位制或十六進位制的數字。

如果 string 以 1 ~ 9 的數字開頭,parseInt() 將把它解析為十進位制的整數。 所以最後結果是[1, NaN, NaN]也就不足為奇了

那麼如何解決這個問題,返回預期的結果, 估計很多人不假思索都會想出這種方式

["1","2","3"].map(v => parseInt(v));
複製程式碼

沒錯,的確可以得到正確的結果,簡單粗暴,直接明瞭,但是這種方式是不是可以稍微封裝一下,具有擴充套件性 在函數語言程式設計中,可以用這個一元轉換函式包裹目標函式,確保目標函式只會接受一個引數

function unary(fn) {
    return function onlyOneArg(arg){
        return fn(arg);
    };
}
["1","2","3"].map( unary(parseInt) ); // [1,2,3]
複製程式碼

3. 偏函式partial

固定一個函式的一個或者多個引數,返回一個新的函式,這個函式用於接受剩餘的引數, 和map結合使用比較多

    function partial(fn, ...presetArgs) {
        return function partiallyApplied(...laterArgs ) {
           return fn(...presetArgs, ...laterArgs )
       } 
    }
    // 應用1
    var getPerson = partial( ajax, "http://some.api/person" )
    var getOrder = partial( ajax, "http://some.api/order" )
    // version1
    var getCurrentUser = partial(ajax, "http://some.api/person", {user: "hello world"})
    // version 2
    var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } );
    // 應用2
    function add(x, y){
        return x + y
    }
    [1,2,3,4,5].map( partial( add, 3 ) )
複製程式碼
    function partialRight(fn, ...presetArgs) {
        return function partiallyApplied(...laterArgs) {
            return fn(...laterArgs, ...presetArgs)
        }   
    }
複製程式碼

4. 科裡化 curry

柯里化是一種將使用多個引數的一個函式轉換成一系列使用一個引數的函式的技術。

    function add(a, b) {
        return a + b;
    }
    
    // 執行 add 函式,一次傳入兩個引數即可
    add(1, 2) // 3
    // 假設有一個 curry 函式可以做到柯里化
    var addCurry = curry(add);
    addCurry(1)(2) // 3
複製程式碼

下面是一個簡單的實現

function sub_curry(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat([].slice.call(arguments)));
    };
}

function curry(fn, length) {
    length = length || fn.length;
    var slice = Array.prototype.slice;
    return function() {
        if (arguments.length < length) {
            var combined = [fn].concat(slice.call(arguments));
            return curry(sub_curry.apply(this, combined), length - arguments.length);
        } else {
            return fn.apply(this, arguments);
        }
    };
}



複製程式碼

5. 組合 compose

函式組合,將一個函式的輸出當成另外一個函式的輸入,讓資料流可以像水在水管中流動一樣,為了組合,必須保證組合的函式引數只能有一個,而且必須有返回值
複製程式碼
    // 執行順序從右向左
    function compose(...fn) {
        return function composed(result){
            var list = [...fn]
            while(list.length > 0) {
                result = list.pop()(result)
            }
            return result
        }
    }
    // 管道函式, 從左向右移動
    function pipe(...fn) {
        return function piped(result) {
            var list = [...fn]
            while(list.length > 0) {
                result = list.shift()(result)
            }
            return result
        }
    }

複製程式碼

6. 遞迴(recursion)

// 判斷一個數是不是素數
function isPrime(num,divisor = 2){
    if (num < 2 || (num > 2 && num % divisor == 0)) {
        return false;
    }
    if (divisor <= Math.sqrt( num )) {
        return isPrime( num, divisor + 1 );
    }
    return true;
}
// 計算二叉樹的深度
function depth(node) {
    if(node) {
            let depthLeft = depth(node.left)
            let depthRight = depth(node.right)
            return 1 + Math.max(depthLeft, depthLeft)
    }
    return 0
}
複製程式碼
  • 遞迴太深,會存在記憶體溢位的問題,需要用尾呼叫來優化
    // 解決棧溢位的問題,尾呼叫優化
    // 尾呼叫的概念非常簡單,就是指某個函式的最後一步是呼叫另一個函式。
    // 下面都不是
    // 情況一
    function f(x){
      let y = g(x);
      return y;
    }
    
    // 情況二
    function f(x){
      return g(x) + 1;
    }
    
    // 階乘函式
    function factorial(n) {
        if( n === 1) {
            return 1
        }
        return n*factorial(n-1)
    }
複製程式碼

將階乘函式改成尾呼叫, 確保最後一步只呼叫自身, 就是把所有用到的內部變數改寫成函式的引數

    function factorial(n, total) {
        if (n===1) {
          return total  
        }
        return factorial(n - 1, n*total)
    }
    // 但是這樣會傳兩個引數,用兩個函式改寫一下
    function factorial(n) {
        return tailFactorial(n ,1)
    }
    function tailFactorial(n, total) {
        if (n===1) {
          return total  
        }
        return tailFactorial(n - 1, n*total)
    }
    
    // 繼續改寫, tailFactorial放在factorial內部
    function factorial(n) {
        function tailFactorial(n, total) {
            if (n===1) {
            return total  
            }
            return tailFactorial(n - 1, n*total)
        }
        return tailFactorial(n ,1)
    } 
    
    // 也可以使用curry函式,將多引數的函式轉換為單引數的形式
    function currying(fn, n) {
      return function (m) {
        return fn(m, n);
      };
    }
    function tailFactorial(n, total) {
        if (n===1) {
          return total  
        }
        return tailFactorial(n - 1, n*total)
    } 
    var factorial = currying(tailFactorial, 1)
    factorial(5)
複製程式碼

相關文章