天天看點

[譯]Functor 與 Category (軟體編寫)(第六部分)

<b>本文講的是[譯]Functor 與 Category (軟體編寫)(第六部分),</b>

所謂 functor(函子),是能夠對其進行 map 操作的對象。換言之,functor 可以被認為是一個容器,該容器容納了一個值,并且暴露了一個接口(譯注:即 map 接口),該接口使得外界的函數能夠擷取容器中的值。是以當你見到 functor,别被其來自範疇學的名字唬住,簡單把他當做個 “mappable” 對象就行。

“functor” 一詞源于範疇學。在範疇學中,一個 functor 代表了兩個範疇(category)間的映射。簡單說來,一個 範疇 是一系列事物的分組,這裡的 “事物” 可以指代一切的值。對于編碼來說,一個 functor 通常代表了一個具有 <code>.map()</code> 方法的對象,該方法能夠将某一集合映射到另一集合。

上文說到,一個 functor 可以被看做是一個容器,比如我們将其看做是一個盒子,盒子裡面容納了一些事物,或者空空如也,最重要的是,盒子暴露了一個 mapping(映射)接口。在 JavaScript 中,數組對象就是 functor 的絕佳例子(譯注:<code>[1,2,3].map(x =&gt; x + 1)</code>),但是,其他類型的對象,隻要能夠被 map 操作,也可以算作是 functor,這些對象包括了單值對象(single valued-objects)、流(streams)、樹(trees)、對象(objects)等等。

對于如數組和流等其他這樣的集合(collections)來說,<code>.map()</code> 方法指的是,在集合上進行疊代操作,在此過程中,應用一個預先指定的函數對每次疊代到的值進行處理。但是,不是所有的 functor 都可以被疊代。

在 Haskell 中,functor 類型被定義為如下形式:

fmap 接受一個函數參數,該函數接受一個參數 <code>a</code>,并傳回一個 <code>b</code>,最終,fmap 完成了從<code>f a</code> 到 <code>f b</code> 的映射。<code>f a</code> 及 <code>f b</code> 可以被讀作 “一個 <code>a</code> 的 functor” 和“一個 <code>b</code> 的 functor”,亦即 <code>f a</code> 這個容器容納了 <code>a</code>,<code>f b</code> 這個容器容納了 <code>b</code>。

使用一個 functor 是非常簡單的,僅需要調用 <code>map()</code> 方法即可:

一個範疇含有兩個基本的定律:

同一性(Identity)

組合性(Composition)

由于 functor 是兩個範疇間的映射,其就必須遵守同一性群組合性,二者也構成了 functor 的基本定律。

如果你将函數(<code>x =&gt; x</code>)傳入 <code>f.map()</code>,對任意的一個 functor <code>f</code>,<code>f.map(x =&gt; x) == f</code>。

functor 還必須具有組合性:<code>F.map(x =&gt; f(g(x))) == F.map(g).map(f)</code>

函數組合是将一個函數的輸出作為另一個函數輸入的過程。例如,給定一個值 <code>x</code>及函數 <code>f</code>和函數 <code>g</code>,函數的組合就是 <code>(f ∘ g)(x)</code>(通常簡寫為 <code>f ∘ g</code>,簡寫形式已經暗示了<code>(x)</code>),其意味着 <code>f(g(x))</code>。

很多函數式程式設計的術語都源于範疇學,而範疇學的實質即是組合。初看範疇學,就像初次進行高台跳水或者乘坐過山車,慌張,恐懼,但是并不難完成。你隻需明确下面幾個範疇學基礎要點:

一個範疇(category)是一個容納了一系列對象及對象間箭頭(<code>-&gt;</code>)的集合。

箭頭隻是形式上的描述,實際上,箭頭代表了态射(morphismms)。在程式設計中,态射可以被認為是函數。

對于任何被箭頭相連接配接的對象,如 <code>a -&gt; b -&gt; c</code>,必須存在一個 <code>a -&gt; c</code> 的組合。

所有的箭頭表示都代表了組合(即便這個對象間的組合隻是一個同一(identity)箭頭:<code>a-&gt;c</code>)。所有的對象都存在一個同一箭頭,即存在同一态射(<code>a -&gt; a</code>)。

如果你有一個函數 <code>g</code>,該函數接受一個參數 <code>a</code> 并且傳回一個 <code>b</code>,另一個函數 <code>f</code> 接受一個<code>b</code> 并傳回一個 <code>c</code>。那麼,必然存在一個函數 <code>h</code>,其代表了 <code>f</code> 及 <code>g</code> 的組合。而 <code>a -&gt; c</code>的組合,就是 <code>f ∘ g</code>(讀作<code>f</code> 緊接着 <code>g</code>),進而,也就是 <code>h(x) = f(g(x))</code>。函數組合的方向是由右向左的,這也就是就是 <code>f ∘ g</code> 常被叫做 <code>f</code> 緊接着 <code>g</code> 的原因。

函數組合是滿足結合律的,這就意味着你在組合多個函數時,免去了添加括号的煩惱:

讓我們再看一眼 JavaScript 中組合律:

給定一個 functor,<code>F</code>:

下面的兩段是等效的:

譯注:functor 中函數組合的結合率可以被了解為:對 functor 中儲存的值使用組合後的函數進行 map,等效于先後對該值用不同的函數進行 map。

一個 endofunctor(自函子)是一個能将一個範疇映射回相同範疇的 functor。

一個 functor 能夠完成任意範疇間映射: <code>F a -&gt; F b</code>

一個 endofunctor 能夠完成相同範疇間的映射:<code>F a -&gt; F a</code>

在這裡,<code>F</code> 代表了一個 functor 類型,而 <code>a</code> 代表了一個範疇變量(意味着其能夠代表任意的範疇,無論是一個集合,還是一個包含了某一資料類型所有可能取值的範疇)。

而一個 monad 則是一個 endofunctor,先記住下面這句話:

“monad 是 endofunctor 範疇的 monoids(幺半群),有什麼問題?”(譯注:這句話的出處在該系列第一篇已有提及)

現在,我們希望第一篇提及的這句話能在之後多一點意義,monoids(幺半群)及 monad 将在之後作介紹。

下面将展示一個簡單的 functor 例子:

顯然,其滿足了 functor 定律:

現在,你可以對存在該 functor 中的任何資料類型進行 map 操作,就像你對一個數組進行 map 時那樣。這簡直太美妙了。

上面的代碼片展示了 JavaScript 中 functor 的簡單實作,但是其缺失了 JavaScript 中常見資料類型的一些特性。現在我們逐個添加它們。首先,我們會想到,假如能夠直接通過 + 操作符操作我們的 functor 是不是很好,就像我們在數值或者字元串對象間使用 <code>+</code> 号那樣。

為了使該想法變現,我們首先要為該 functor 對象添加 <code>.valueOf()</code> 方法 —— 這可被看作是提供了一個便捷的管道來将值從 functor 盒子中取出。

現在代碼更漂亮了。但是如果我們還想要在控制台審查 <code>Identity</code> 執行個體呢?如果控制台能夠輸出 <code>"Identity(value)"</code> 就太好了,為此,我們隻需要添加一個 <code>.toString()</code> 方法即可(譯注:亦即重載原型鍊上原有的 <code>.toString()</code> 方法):

現在,我們的 functor 還能這樣工作:

假如你想借助 <code>Identity(n)</code> 來傳回包含了 <code>n+1</code>,<code>n+2</code> 等等的 Identity 數組,這非常容易:

但是,如果你想上面的操作方式能夠應用于任何 functor,該怎麼辦?假如我們規定了每種資料類型對應的執行個體必須有一個關于其構造函數的引用,那麼你可以這樣改造之前的邏輯:

假如你還想知道一個值是否在一個 functor 中,又怎麼辦?我們可以為 <code>Identity</code> 添加一個靜态方法 <code>.is()</code> 來進行檢測,另外,我們也順便添加了一個靜态的 <code>.toString()</code> 方法來告知這個 functor 的種類:

現在,我們整合一下上面的代碼片:

注意,無論是 functor,還是 endofunctor,不一定需要上述那麼多的條條框框。以上工作隻是為了我們在使用 functor 時更加便捷,而非必須。一個 functor 的所有需求隻是一個滿足了 functor 定律 <code>.map()</code> 接口。

說 functor 多麼多麼好不是沒有理由的。最重要的一點是,functor 作為一種抽象,能讓開發者以同一種方式實作大量有用的,能夠操縱任何資料類型的事物。例如,如果你想要在 functor 中值不為 <code>null</code> 或者不為 <code>undefined</code> 前提下,建構一串地鍊式操作:

你可以使用自己喜歡的 curry 化方法(譯注:Underscore,Lodash,Ramda 等第三方庫都提供了 curry 化一個函數的方法),或者使用下面這個之前篇章提到的,基于 ES6 的,充滿魅力的 curry 化方法來實作參數的部分應用:

現在,我們可以自定義 <code>map()</code> 方法:

functor 是能夠對其進行 map 操作的對象。更進一步地,一個 functor 能夠将一個範疇映射到另一個範疇。一個 functor 甚至可以将某一範疇映射回相同範疇(例如 endofunctor)。

一個範疇是一個容納了對象和對象間箭頭的集合。箭頭代表了态射(也可了解為函數或者組合)。一個範疇中的每個對象都具有一個同一态射(<code>x -&gt; x</code>)。對于任何連結起來的對象 <code>A -&gt; B -&gt; C</code>,必存在一個 <code>A -&gt; C</code> 的組合。

總之,functor 是一個極佳的高階抽象,能然你建立各種各樣的通用函數來操作任何的資料類型。

未完待續……

<b></b>

<b>原文釋出時間為:2017年4月18日</b>

<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>

繼續閱讀