JS函數式編程【譯】5.2 函子 (Functors)


函子(Functors)

態射是類型之間的映射;函子是范疇之間的映射。可以認為函子是這樣一個函數,它從一個容器中取出值, 並將其加工,然后放到一個新的容器中。這個函數的第一個輸入的參數是類型的態射,第二個輸入的參數是容器。

函子的函數簽名是這個樣子
// myFunctor :: (a -> b) -> f a -> f b
意思是“給我一個傳入a返回b的函數和一個包含a(一個或多個)的容器,我會返回一個包含b(一個或多個)的容器”

創建函子

要知道我們已經有了一個函子:map(),它攫取包含一些值的容器(數組),然后把一個函數作用於它。

[1, 4, 9].map(Math.sqrt); // Returns: [1, 2, 3]

然而我們要把它寫成一個全局函數,而不是數組對象的方法。這樣我們后面就可以寫出簡潔、安全的代碼。

// map :: (a -> b) -> [a] -> [b]
var map = function(f, a) {
  return arr(a).map(func(f));
}

這個例子看起來像是個故意弄的封裝,因為我們只是把map()函數換了個形式。但這有它的目的。 它為映射其它類型提供了一個模板。

// strmap :: (str -> str) -> str -> str
var strmap = function(f, s) {
  return str(s).split('').map(func(f)).join('');
}

數組和函子

數組是函數式JavaScript使用數據的最好的方式。

是否有一種簡單的方法來創建已經分配了態射的函子?有,它叫做arrayOf。 當你傳入一個以整數為參數、返回數組的態射時,你會得到一個以整數數組為參數返回數組的數組的態射。

它自己本身不是函子,但是它讓我們能夠用態射建立函子。

// arrayOf :: (a -> b) -> ([a] -> [b])
var arrayOf = function(f) {
  return function(a) {
    return map(func(f), arr(a));
  }
}

下面是如何用態射創建函子

var plusplusall = arrayOf(plusplus); // plusplus是函子
console.log( plusplusall([1,2,3]) ); // 返回[2,3,4]
console.log( plusplusall([1,'2',3]) ); // 拋出錯誤

函數組合,重訪(revisited)

函數也是一種我們能夠用函子來創建的原始類型,這個函子叫做“fcompose”。我們對函子是這樣定義的: 它從容器中取一個值,並對其應用一個函數。如果這個容器是一個函數,我們只需要調用它並獲取里面的值。

我們已經知道了什么是函數組合,不過讓我們來看看在范疇論驅動的環境里它們能做些什么。

函數組合就是結合(associative,中學數學中學到的“結合律”中的“結合”)。如果你的高中代數老師也像我這樣的話那她只告訴了你函數組合的定律有什么,而沒有沒教你用它能做些什么。在實踐中,組合就是結合律所能夠做的。

(a × b) × c = a × (b × c)
(f  g)  h = f  (g  h)

f  g ≠ g  f

我們可以任意進行內部組合,無所謂怎樣分組。交換律也沒有什么可迷惑的。f g 不總等於 g f。比如說,一個句子的第一個單詞被反轉並不等同於一個被反轉的句子的第一個單詞。

總的來說意思就是哪個函數以什么樣的順序被執行是無所謂的,只要每個函數的輸入來源於上一個函數的輸出。不過,等等,如果右邊的函數依賴於左邊的函數,不就是只有一個固定的求值順序嗎?從左到右?是的,如果把它封裝起來,我們就可以按照我們感覺合適的方式來控制它。這就使得在JavaScript中可以實現惰性求值。

(a × b) × c = a × (b × c)
(f  g)  h = f  (g  h)

我們來重寫函數組合,不作為函數原型的擴展,而是作為一個單獨的函數,這樣我們就可以的到更多的功能。基本的形式是這樣的:

var fcompose = function(f, g) {
  return function() {
    return f.call(this, g.apply(this, arguments));
  };
};

不過我們還得讓它能接受任意數量的輸入。

var fcompose = function() {
  // 首先確保所有的參數都是函數
  var funcs = arrayOf(func)(arguments);  //譯注:這句有問題,見下面注釋
  // 返回一個作用於所有函數的函數
  return function() {
    var argsOfFuncs = arguments;
    for (var i = funcs.length; i > 0; i -= 1) {
      argsOfFuncs  = [funcs[i].apply(this, args)];
    }
    return args[0];
  };
};

// 例:
var f = fcompose(negate, square, mult2, add1);
f(2); // 返回: -36

給原著勘誤:如果你copy上面的代碼執行的話現在肯定看到報錯了,上面這段代碼里的錯誤還真不少……

首先會得到一個錯誤:“Uncaught TypeError: Error: Array expected, something else given.”。 哪個數組沒通過類型驗證呢?是fcompose里的arguments。我在最新版本的chrome和火狐里得到arguments的字符串是[object Arguments], 而且arguments並沒有繼承Array,也就沒有map之類的方法,所以這里需要先把arguments轉換成數組,把fcompose函數體第一句改成這樣就行:
var funcs = arrayOf(func)(Array.prototype.slice.call(arguments));

然后第二個錯誤,低級錯誤,argsOfFuncs和args是一個東西,統一成一個變量名就行了。比如說把argsOfFuncs都改成args吧。 順便說一下這里的意思,首先把初始參數賦給args,然后遍歷組合函數的數組,每執行一個函數就把返回值賦給args, 這樣下一個函數就能把上一個函數的執行結果作為輸入參數了。注意每次的返回值都放到了數組里,是為了符合apply的參數形式, 而最后返回時只要取args里的第一個(也是唯一一個)值就行了。

第三個錯誤,還是低級錯誤,遍歷funcs的時候計數寫成了length到1,而實際上我們需要length-1到0。 順便說下為什么計數要從大到小呢?因為組合的函數要從右往左執行。

最后,上正確的代碼:

var fcompose = function() {
  var funcs = arrayOf(func)(Array.prototype.slice.call(arguments));
  return function() {
    var args = arguments;
    for (var i = funcs.length-1; i >= 0; i -= 1) {
      args  = [funcs[i].apply(this, args)];
    }
    return args[0];
  };
};

現在我們封裝好了這些函數並可以控制它們了。我們重寫了組合函數使得每一個函數接受另一個函數作為輸入, 存儲起來,並同樣返回一個對象。這里並不是接受一個數組作為輸入處理它,而是對每一個操作返回一個新的數組, 我們可以在源頭上讓每一個元素接受一個數組,把所有操作合到一起執行(所有map、filter等等組合到一起), 最終把結果存到一個新數組里。這就是通過函數組合實現的惰性求值。這里我們沒有理由重新造輪子, 許多庫對於這個概念都有很好的實現,包括Lazy.js、Bacon.js以及wu.js等庫。

利用這一不同模式的結果,我們可以做更多事情:異步迭代、異步事件處理、惰性求值甚至自動並行。

自動並行?在計算機科學界有一個詞叫做:IMPOSSIBLE。但是這真的不可能嗎? 摩爾定律的下一個飛躍沒准是一個能夠將我們的代碼並行化的編譯器,函數組合能做到嗎? 不,這行不通。JavaScript引擎實現並行化並不是自動的,而是依靠精心設計的代碼。 函數組合只是提供了切分成並行進程的機會。但是它本身已經足夠酷了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM