先看一下昨天最後的範例
import Control.Applicative
let foo = fmap (*) [1..5]
let result = fmap (\f x -> f x) foo <*> pure 2
result -- [2,4,6,8,10]
其實還有更簡單的寫法,或者該說更 applicative style 的寫法
pure (*) <*> [1..5] <*> pure 2
這次我們改先用 pure (*)
這時我們可以把它想像成他是一個被 Applicative
包住的 (*)
實際上他的 type 是
pure (*) :: (Applicative f, Num a) => f (a -> a -> a)
然後我們用 <*> [1..5]
,原本是 f (a -> a -> a)
經過 <*>
就會變成 [ a → a ]
。
因為 <*>
是將 functor 裡面的 function 拿出來 apply 到另一個 functor 裡,所以原本 (a→a→a)
因為已經傳入一個參數,所以就變成了 (a → a)
然後最後 wrap 住我們的 functor 也就是 []
。
最後我們再呼叫一次 pure 2
,一樣 pure 2
就是一個被 functor wrap 住的 2,也就只是一個單純的 (Applicative f, Num a) => f a
,所以我就可以簡單地使用 <*>
將這 f(a→b)
apply 到另外一個 functor 上,所以最後就是 [2,4,6,8,10]
用比較不精確的描述的話大概會像是我們第一次使用 <*>
時是將這個 list 變成 [1*, 2* .. 5*]
的形式
當然這只是為了方便解釋,而且它可能會更像
[(*) 1 , (*)2..]
的形式
但我們現在獲得一個 f (a→b)
顯然的我無法直接讓他跟 2
進行運算,所以我就必須使用 pure 2
讓它也被 functor wrap 。之後我就可以直接使用 <*>
讓這兩個 functor 可以進行運算。
昨天我們有提到如果是想要 Maybe 相加該怎麼辦呢?感覺就算我們不用 Applicative
好像也能達成,而且寫起來感覺沒有 list 複雜
addMaybe :: Maybe Int -> Maybe Int -> Maybe Int
addMaybe Nothing _ = Nothing
addMaybe _ Nothing = Nothing
addMaybe (Just x) (Just y) = Just (x + y)
我們先用我們的老朋友 pattern matching 試著操作看看,這裡很簡單的就是前面兩個就是匹配到任一個 Nothing
時回傳值就會是 Nothing
,而最後一行則就是當我們匹配到兩個 Just
時,就將 Just
裡的 value 取出來相加後再用 Just
wrap 住。
這樣寫的缺點顯而易見,我們針對不同的 functor 就可能要寫出不一樣的 function ,針對不一樣的運算也必須寫出不一樣的 function 。
所以我們一樣可以使用 Applicative
優雅地操作 Maybe
Just (+1) <*> Just 2 -- Just 3
(+) <$> Just 1 <*> Just 2 -- Just 3
pure (+) <*> Just 1 <*> Just 2 -- Just 3
pure (+) <*> Just 1 <*> pure 2 -- Just 3
pure (+) <*> Just 1 <*> Nothing -- Nothing
首先第一行很簡單,Just (+1)
就是 f (a→b)
所以直接用 <*>
apply 到 Just 2
第二行的 <$>
其實就是等於 fmap
所以也就是等於 fmap (+) Just 1
,沒錯又是一個 f (a→b)
。
至於後面的就有點大同小異,如果要把一個純粹的 valuable 跟 functor 進行運算就是先用 pure
讓他一樣是被 functor wrap 住,方便我們使用 <*>
來 apply 到我們要運算的 functor。
所以 Applicative
或者該說 functor 到底解決了什麽問題?我覺得這些語法或者該說 typeclass 提供了一個統一的介面處理這些「可以被 map 的東西」,那「可以被 map 的東西」到底哪裡重要了?首先 list 算是我們相當常用的資料結構,以及 Maybe 就是當我們對於某些不確定的數值時會去使用的 typeclass ,也就是不管是管理數值的資料結構或者處理例外處理,對於 Haskell 來說都是「可以被 map 的東西」。所以勢必是要提供這些介面方便開發者去操作。
雖然我覺得這可能有點倒果為因,更應該是剛好數學上的特性導致我們可以這樣利用 Applicative 及 functor
再次提醒雖然我不斷使用 「wrap」 來表達 functor f => f a
這種形式,但我只是在說明 a
處於某種 context 中,不代表它真正被什麼東西或者資料結構給 wrap 了
今天的程式碼:https://github.com/toddLiao469469/30days-for-haskell
雖然與Applicative
無關,但我們也可以利用Maybe
及Int
加法具有Monoid
的特性來換個方式實作addMaybe
。
import Data.Coerce (coerce)
import Data.Monoid (Sum(Sum))
addMaybe :: Maybe Int -> Maybe Int -> Maybe Int
addMaybe = coerce $ (<>) @(Maybe (Sum Int))
這絕非是在炫技
一時半刻也看不出來它的意思 xD
但這個思路還蠻有趣的,利用 Semigroup
的 <>
來達成這件事情。
只是我有點好奇雖然 Monoid
一定會是 Semigroup
,但既然是 Monoid
直覺應該會想到 mappend
為什麼這邊會是使用 <>