除了Array Functor,我們昨天又介紹了Option和Either Functor兩個Functor,要成為Functor,最重要的是要在模組裏實作map這個函數。假設我們的Functor型別建構子為F,我們來看看map函數的型別定義:map: <A, B>(f: A => B) => (fa: F<A>) => (fb: F<B>)
而這個函數型別定義說,如果你的輸入是一個從型別A到型別B的函數,輸出會是一個型別F<A>
到F<B>
的函數,我們把型別A和型別B所在的世界看成地面,而把型別建構子F所建構的型別世界看成空中,我們說map這個函數把原來的f提升(Lifting),但這map的提升是有限制的,它只能提升只有一個輸入的函數。讓我們來看看map提升Currying多輸入函數會有什麼樣的不同?我們先看看最熟的Array Functor。
import { of, map } from 'fp-ts/Array'
import { pipe, flow } from 'fp-ts/function'
type Add = (x: number) => (y: number) => number;
const add: Add = (x) => (y) => x + y;
const addResult = pipe(
of(3), //[3]
map(add), // [y => 3 + y]
map(f => f(4)) // [7]
)
上面的程式碼如果將
fp-ts/Array
改成fp-ts/Option
,我們得到的便是some(3), some(y => y + 3), some(7)。
假設因為add(x)會得到一個(y) => x + y的函數,根據Functor map的型別定義,pipe(of(3), map(add))等同得到一個of(y => 3 + y),這時候我們如果想要再加4,只好再map(f => f(4)),這樣子顯得非常笨拙,我們希望能夠有一個函數從陣列[y => 3 + y]中取得f: y => y + 3這個f函數,然後從陣列[4]中取得4,直接執行f(4),這個函數稱之為ap,Array Functor的ap的定義如下:
ap: <A>(fa: A[]) => <B>(fab: ((a: A) => B)[]) => B[]
上面的程式我們便可改寫成
import { of, map, ap } from 'fp-ts/Array'
import { pipe, flow } from 'fp-ts/function'
type Add = (x: number) => (y: number) => number;
const add: Add = (x) => (y) => x + y;
const addResult = pipe(
of(3), //[3]
map(add), // [y => 3 + y]
ap(of(4)) // [7]
)
ap函數的目地是將我們包在函數和參數的容器(陣列)解開,讓函數直接作用在參數後,再將得到的結果放入容器(陣列)。上面addResult可以改寫成另一種格式
const addResult = pipe(
of(add), //[x => y => x + y]
ap(of(3)), // [y => 3 + y]
ap(of(4)) // [7]
)
這個ap函數讓實現了數學式of(f)(of(3))(of(4)),如果我們將of(f)記作 fₐ ,of(3)記作 3ₐ ,of(4)記作 4ₐ ,我們便實現了以下的式子fₐ(3ₐ)(4ₐ)的概念,這個概念的意義是我們把原來的 f 對應到空間 A 中的 fₐ ,而 3 和 4 分別對應到空間 A 中的 3ₐ 和記作 4ₐ,而3ₐ 和 4ₐ 在 fₐ 的定義域中,因此可以執行 fₐ(3ₐ)(4ₐ)而其結果應該等於先執行f(3)(4)再對應到空間 A 中的值。在程式設計的角度,fₐ、3ₐ和4ₐ都被嵌套在容器內,並沒有辦法直接進行數學上的計算,而必須有一個解嵌套的過程,也就是必須ap這個函數完成這個功能。
這種型式可以輕鬆的拓展到更多輸入的函數的提升。
type Add3 = (x: number) => (y: number) => (z: number) => number;
const add3: Add3 = (x) => (y) => (z) => x + y + z;
const add3Result = pipe(
of(add3), //[x => y => x + y]
ap(of(3)), // [y => 3 + y],of(3) 就 [3]
ap(of(4)), // [7]
ap(of(5)), // [12]
)
如果作用的參數一旦遇到空陣列([]),不論後面的參數是什麼陣列內容,我們得到的結果都是空陣列。
在函數式程式設計的世界裏,實作map函數的型別類別(Type Class)就稱為Functor,一個實作ap函數的Functor就稱為Apply Functor,而實作of的Apply Functor就是Applicative Functor。fp-ts所提供的模組都提供了這3個函數,甚至更多(明天要介紹的Monad會實作另一個函數),所以我們目前並不需要特別去區別三者,只要知道每個函數的用法即可。
我們也不在這邊說明不同的Functor如何實作這幾個函數,我們要學習的是如何使用它們,事實上,不同的Functor的抽像概念相同,我們可以想像這些Functors都是不同的容器,而它們的使用方法也都一樣,命名大多也相同,所以只要學會一個容器的用法,便可以推廣至其它Functor上面。例如上面的例子,如果of和ap是從fp-ts/Option匯出,那我們得到的結果便如下:
import { of, ap, none } from 'fp-ts/Option';
const add3Result = pipe(
of(add3), //some(x => y => z => x + y + z)
ap(of(3)), // some(y => 3 + y + z)
ap(of(4)), // some(z => 7 + z)
ap(of(5)), // some(12)
)
和Array Functor一樣,一旦ap作用在none上,不論後面的參數是什麼,最後都得到none。
const add3ResultWithNone = pipe(
of(add3), //some(x => y => z => x + y + z)
ap(of(3)), // some(y => 3 + y + z)
ap(none), // none
ap(of(5)) // none
);
這邊要特別一提的是Functor Array的ap,由於Array這個容器和其它容器最大的不同是它存有值的數量是不確定的(nondeterministic),因此,如果ap作用的陣列元素超過1個,則每一個元素都會對下一個ap參數陣列中的每一個元素作用,我們用下面的例子來說明。
const addResult = pipe(
of(add), //[x => y => x + y]
ap([2, 3]), // [y => 2 + y, y => 3 + y]
ap([4, 6, 8]) // [6, 8, 10, 7, 9, 11]
)
對於單值的型別建構容器則沒有這個困擾,程式結構幾乎完全一樣,像Either作用在left的效果和Option作用在none一樣。
const add3EitherResult = pipe(
E.of(add3), //right(x => y => z => x + y + z)
E.ap(E.of(3)), // right(y => 3 + y + z)
E.ap(E.of(4)), // right(z => 7 + z)
E.ap(E.of(5)) // right(12)
);
console.log(add3EitherResult);
const add3EitherResultWithLeft = pipe(
E.of(add3), //right(x => y => z => x + y + z)
E.ap(E.of(3)), // right(y => 3 + y + z)
E.ap(E.left(4)), // left(4)
E.ap(E.of(5)) // left(4)
);
console.log(add3EitherResultWithLeft);
ap函數讓我們將封存在容器內的函數能解開型別容器的嵌套,將封存在相同型別容器的參數取出進而計算函數的結果,再封存至相同的型別容器內,而有了隔空取物的效果。在ap的過程中如果遇到函數的異常值(Option中的none或Either中的left),則會停止函數的執行,而異常值則會傳播下去。
具有map和ap函數的介面(interface)稱作Apply,如果再多實作of函數則稱為Applicative,這兩個介面在空間的穿越(Traverse)中是必要的參數,未來的發文我們會再介紹Traverse,今天分享的內容就到此為止,明天再見。