《Haskell趣學指南》筆記之 Applicative 函子
class Functor f where fmap :: (a -> b) -> f a -> f b instance Functor IO where fmap f action = do result <- action return (f result) 複製程式碼
示例:
main = do line <- getLine let line' = reverse line putStrLn $ "You said " ++ line' ++ " backwards!" -- 下面使用 fmap 改寫上面的程式碼 main = do line <- fmap reverse getLine putStrLn $ "You said " ++ line ++ " backwards!" -- 回顧一下列表的函子特性 ghci> fmap (*2) [1.. 3] [2, 4, 6] 複製程式碼
作為函子的函式
Haskell 的函式型別定義會出現 -> 符號,比如程式碼 1:
class Functor f where fmap :: (a -> b) -> f a -> f b 複製程式碼
這個 -> 其實也是函子,其定義在Control.Monad.Instances
裡:
instance Functor ((->) r) where fmap f g = (\x -> f (g x)) 複製程式碼
仔細觀察就會發現,這個 fmap 實際上就是函式組合。所以
instance Functor ((->) r) where fmap = (.) 複製程式碼
如果你想不通,可以把程式碼 1 的 f 改成 (->) r,你就得到了
fmap :: (a -> b) -> (->) r a -> (->) r b 複製程式碼
然後把 (->) r a 改成 r -> a,得到程式碼 2
fmap :: (a -> b) -> (r -> a) -> (r -> b) 複製程式碼
這個 fmap 接受 f1(a->b) 和 f2(r-a),得到一個新的函式 f3; f3 接受一個 r,通過 f2 把 r 變成 a,然後通過 f1 把 a 變成 b;這不就是函式組合麼?
ghci> fmap (*3) (+100) 1 303 ghci> (*3) . (+100) $ 1 303 複製程式碼
問題在於,我現在無法把上面的程式碼與列表做對比了:
ghci> fmap (*2) [1.. 3] [2, 4, 6] 複製程式碼
列表很容易理解成容器,但是 (+100) 到底是什麼容器呢?如果看程式碼 2 的話,說不定能理解一點:
fmap :: (a -> b) -> (r -> a) -> (r -> b) 複製程式碼
(r ->) 就是一個容器吧。或者容器這個比喻在這裡已經不適用了。
另一個角度看
如果一個函式的型別是a -> b -> c
,就表示它接受一個 a 型別的值,返回一個b -> c
函式。所以a -> b -> c
等價於a -> (b -> c)
。那麼fmap :: (a -> b) -> (f a -> f b)
可以寫成這樣,也就是說
fmap 接受 a -> b 型別的函式,返回 f a -> f b 型別的函式,其中 f 接受型別變數。具體化一下就是(Int -> String) -> (Maybe Int -> Maybe String)
也就是說fmap (*2) [1.. 3]
裡的(*2)
是 Int -> Int,經過 fmap 一折騰,變成了 [Int] -> [Int]。這種操作叫做提升(lifting)一個函式。
兩種方式思考 fmap
-
(a -> b) -> f a -> f b
接受函式(*2)
和函子值[1,2,3]
,在函子值上對映這個函式 -
(a -> b) -> (f a -> f b)
接受函式,把它提升為操作函子值的函式兩種看法都對。
Applicative
先看定義
class (Functor f) => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b 複製程式碼
<*> <*>
使用示例:
instance Applicative Maybe where pure = Just Nothing <*> _ = Nothing (Just f) <*> something = fmap f something ghci> Just (+3) <*> Just 9 Just 12 ghci> pure (+3) <*> Just 9 Just 12 複製程式碼
為了方便對比,我把 Maybe 作為 Functor 例項的程式碼也複製過來
instance Functor Maybe where fmap f Nothing = Nothing fmap f (Just x) = Just (f x) 複製程式碼
那麼instance Applicative Maybe
的意思就很明顯了:
- pure = Just 的意思是,如果你想把一個值裝到 Maybe 裡,用 Just 接受這個值即可
-
<*> Nothing _ = Nothing
是說,如果第一個盒子裡沒值,那麼得到的盒子裡一定也沒值 -
<*> (Just f) something = fmap f something
是說,如果一個盒子裡是 f,另一個盒子裡不管是什麼,<*>
的功能都是- 拿出 something 裡的值
- 用 f 呼叫這個值
- 把值返回之前跟同類的盒子裡這裡我不太明白的地方在於,something 就沒有什麼約束嗎?
Applicative Style
我們可以用<*>
把一個函式和兩個值連起來(但是要注意順序,而且<*>
是左結合的)
λ> :t pure (+) <*> Just 3 Just (+) <*> Just 3 :: Num a => Maybe (a -> a) λ> pure (+) <*> Just 3 <*> Just 5 Just 8 λ> pure 3 <*> Just (+) <*> Just 5 error... 複製程式碼
為什麼要用 pure 開頭呢?因為 pure 會把東西放在預設的上下文中(也就是 Just)。根據定義(Just f) <*> something = fmap f something
pure (+) <*> Just 3
可以改寫為fmap (+) (Just 3)
因此 Control.Applicative 有一個<$>
函式,實際上就是 fmap 的中綴版本:
(<$>) :: (Functor f) => (a -> b) -> f a -> f b f <$> x = fmap f x 複製程式碼
用上<$>
之後,整個過程就更簡潔了:如果想把 f 對映到兩個 Application 例項的值上,可以寫成
f <$> x <*> y 複製程式碼
如果想把 f 對映到兩個普通值上,可以寫成
f x y 複製程式碼
列表也是 Applicative 的例項
instance Applicative [] where pure x = [x] fs <*> xs = [f x | f <- fs, x <- xs] ghci> [(*0),(+ 100),(^ 2)] <*> [1, 2, 3] [0, 0, 0, 101, 102, 103, 1, 4, 9] ghci> [(+),(*)] <*> [1, 2] <*> [3, 4] [4, 5, 5, 6, 3, 4, 6, 8] ghci> (++) <$> ["ha"," heh"," hmm"] <*> ["?","!","."] ["ha?"," ha!"," ha."," heh?"," heh!"," heh."," hmm?"," hmm!"," hmm."] ghci> [ x* y | x <- [2, 5, 10], y <- [8, 10, 11]] [16, 20, 22, 40, 50, 55, 80, 100, 110] ghci> (*) <$> [2, 5, 10] <*> [8, 10, 11] [16, 20, 22, 40, 50, 55, 80, 100, 110] ghci> filter (>50) $ (*) <$> [2, 5, 10] <*> [8, 10, 11] [55, 80, 100, 110] 複製程式碼
IO 也是 Applicative 的例項
instance Applicative IO where pure = return a <*> b = do f <- a x <- b return (f x) myAction :: IO String myAction = pure (++) <*> getLine <*> getLine-- 也可以寫成 (**) <$> getLine <*> getLine -- myAction 等價於 myAction :: IO String myAction = do a <- getLine b <- getLine return $ a ++ b 複製程式碼
(->) r 也是 Application 的例項
instance Applicative ((->) r) where pure x = (\_ -> x) f <*> g = \x -> f x (g x) 複製程式碼
說實話沒看懂,但是示例看得懂:
ghci> :t (+) <$> (+3) <*> (*100) (+) <$> (+3) <*> (*100) :: (Num a) => a -> a ghci> (+) <$> (+3) <*> (*100) $ 5 508 複製程式碼
這個函式把+ 用在(+ 3) 和(* 100) 的結果上,然後返回。對於(+) <$> (+3) <*> (*100) $ 5
,(+ 3) 和(* 100) 先被應用到 5 上,返回 8 和 500,然後+ 以這兩個值為引數被呼叫,返回 508。
ghci> (\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 5 [8. 0, 10. 0, 2. 5] 複製程式碼
實用函式
ghci> liftA2 (:) (Just 3) (Just [4]) Just [3, 4] ghci> (:) <$> Just 3 <*> Just [4] Just [3, 4] ghci> sequenceA [Just 3, Just 2, Just 1] Just [3, 2, 1] ghci> sequenceA [Just 3, Nothing, Just 1] Nothing 複製程式碼