iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0

https://ithelp.ithome.com.tw/upload/images/20250831/20118113BLkCodRJyL.png

Applicative Functor

除了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]
)

如果作用的參數一旦遇到空陣列([]),不論後面的參數是什麼陣列內容,我們得到的結果都是空陣列。

Functor, Apply, Applicative

在函數式程式設計的世界裏,實作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,今天分享的內容就到此為止,明天再見。


上一篇
Day 12. 錯誤處理 - Option & Either
下一篇
Day 14. 化同存異 - Monad Functior
系列文
數學老師學函數式程式設計 - 以fp-ts啟航20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言