7 min to read
CIS 194 11 Applicative functors, Part II 應用函子,第二部分
Source: 11-applicative2
▌Applicative functors, Part II 應用函子,第二部分
CIS 194 第 11 週
2012 年 4 月 1 日
建議閱讀:
- Applicative Functors from Learn You a Haskell
- The Typeclassopedia
我們首先回顧 Functor
和 Applicative
類:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
每一個 Applicative
也是 Functor
—— 那麼我們可以根據 pure
和 (<*>)
實現 fmap
嗎?我們試試吧!
fmap g x = pure g <*> x
好,至少具有正確的類型! 但是,不難想像為某種類型創建 Functor
和 Applicative
實例,但相等性不成立。 由於這將是一個相當可疑的情況,因此我們為這種相等性制定一條法則 —— 這是一種正式的方式來說明給定類型的 Functor
和 Applicative
實例必須“很好地配合”
現在,讓我們看看更多 Applicative
實例的示例
▌More Applicative Examples 更多應用示例
Lists 列表
列表的 Applicative
實例如何? 實際上有兩種可能的實例:一種將元素列表的函數列表和參數列表匹配(即,將它們“壓縮”在一起),另一種實例以所有可能的方式組合函數和參數
首先,讓我們編寫一個可以進行所有可能組合的實例。 (原因在下週將變得清楚,這是默認實例)從這個角度來看,列表表示不確定性:也就是說,類型 [a]
的值可以認為是具有多種可能性的單個值。 然後 (<*>)
對應於不確定性函數應用程序—即,不確定性函數對不確定性參數的應用
instance Applicative [] where
pure a = [a] -- a "deterministic" value
[] <*> _ = []
(f:fs) <*> as = (map f as) ++ (fs <*> as)
這是一個例子:
names = ["Joe", "Sara", "Mae"]
phones = ["555-5555", "123-456-7890", "555-4321"]
employees1 = Employee <$> names <*> phones
也許這個特定的例子沒有多大意義,但不難想像您想以各種可能的方式將事物組合在一起的情況。 例如,我們可以像這樣進行非確定性算法:
(.+) = liftA2 (+) -- addition lifted to some Applicative context
(.*) = liftA2 (*) -- same for multiplication
-- nondeterministic arithmetic
n = ([4,5] .* pure 2) .+ [6,1] -- (either 4 or 5) times 2, plus either 6 or 1
-- and some possibly-failing arithmetic too, just for fun
m1 = (Just 3 .+ Just 5) .* Just 8
m2 = (Just 3 .+ Nothing) .* Just 8
接下來,讓我們編寫一個進行元素組合的實例。 首先,我們必須回答一個重要的問題:我們應該如何處理不同長度的列表? 一些想法表明,最明智的做法是將較長的列表截短為較短的列表,丟棄多餘的元素。 當然,還有其他可能的答案:例如,我們可以通過複製最後一個元素來擴展較短的列表(但是,當其中一個列表為空時,我們該怎麼辦?); 或使用 “neutral” 元素擴展較短的列表(但隨後我們將需要 Monoid
實例或應用程序的額外 “default” 參數)
反過來,這個決定決定了我們必須如何執行 pure
,因為我們必須遵守法則
pure f <*> xs === f <$> xs
請注意,右側是與 xs
長度相同的列表,是通過將 f
應用於 xs
中的每個元素形成的。使左側變成相同的唯一方法是 …… 純粹地創建 f
的副本列表,因為我們事先不知道 xs
的長度
我們使用 newtype
包裝器實現實例,以將其與其他列表實例區分開。標準的 Prelude
函數 zipWith
也派上用場
newtype ZipList a = ZipList { getZipList :: [a] }
deriving (Eq, Show, Functor)
instance Applicative ZipList where
pure = ZipList . repeat
ZipList fs <*> ZipList xs = ZipList (zipWith ($) fs xs)
一個例子:
employees2 = getZipList $ Employee <$> ZipList names <*> ZipList phones
Reader / environment 讀者 / 環境
讓我們做一個最後的示例實例,以 (->) e
為例。 這被稱為閱讀器或適用於環境的應用程序,因為它允許從 “環境” 中 “讀取”。 實現實例並不難,我們只需要使用鼻子並遵循以下類型:
instance Functor ((->) e) where
fmap = (.)
instance Applicative ((->) e) where
pure = const
f <*> x = \e -> (f e) (x e)
一個 Employee
例子:
data BigRecord = BR { getName :: Name
, getSSN :: String
, getSalary :: Integer
, getPhone :: String
, getLicensePlate :: String
, getNumSickDays :: Int
}
r = BR "Brent" "XXX-XX-XXX4" 600000000 "555-1234" "JGX-55T3" 2
getEmp :: BigRecord -> Employee
getEmp = Employee <$> getName <*> getPhone
ex01 = getEmp r
▌Aside: Levels of Abstraction 撇開:抽象層次
Functor
是一個漂亮的工具,但是相對簡單。乍一看,Applicative
似乎並沒有增加 Functor
已經提供的功能,但是事實證明,這只是一小部分,具有巨大的影響。適用的(我們將在下週看到,Monad
)應該被稱為“計算模型”,而 Functor
則不能
在處理 Applicative
和 Monad
之類的東西時,請務必記住涉及多個層次的抽象,這一點非常重要。粗略地說,抽像是一種隱藏較低級別細節的東西,它提供了一個“高級”接口,可以(理想情況下)使用該接口,而無需考慮較低級別—儘管較低級別的細節通常會“洩漏”在某些情況下。抽象層的想法很普遍。考慮一下用戶程序-OS-內核-集成電路-門-矽或HTTP-TCP-IP-以太網,或編程語言-字節碼-彙編-機器代碼。如我們所見,Haskell 為我們提供了許多不錯的工具,可在 Haskell 程序本身中構造多層抽象層,即,我們可以向上動態擴展“編程語言”層堆棧。這是一個強大的功能,但可能導致混亂。人們必須學會明確地能夠在多個層次上思考,並在各個層次之間進行切換
特別是對於 Applicative
和 Monad
,只有兩個級別需要關注。 首先是實現各種 Applicative
和 Monad
實例的級別,即 “原始 Haskell” 級別。 在為 Parser
實現 Applicative
實例時,您在以前的作業中獲得了有關此級別的經驗
一旦擁有了諸如 Parser
之類的類型的 Applicative
實例,關鍵是我們可以“向上移動一層”並使用 Applicative
接口使用 Parsers
進行編程,而無需考慮如何實際實現 Parser
及其 Applicative
實例的細節。 您在上週的作業中對此有了一點經驗,本週將獲得更多的經驗。 與實際實現實例相比,在此級別進行編程具有完全不同的感覺。 我們來看一些例子
▌The Applicative API
擁有像 Applicative
這樣的統一接口的好處之一是,我們可以編寫通用的工具和控制結構,以與任何類型的 Applicative
實例一起工作。 作為第一個示例,讓我們嘗試編寫
pair :: Applicative f => f a -> f b -> f (a,b)
pair
接受兩個值並將它們配對,但是所有這些都在某些應用 f
的上下文中進行。 首先嘗試使用配對函數,並使用 (<$>)
和 (<*>)
將其“提升”到參數上:
pair fa fb = (\x y -> (x,y)) <$> fa <*> fb
儘管我們可以簡化一點,但這是可行的。首先,請注意,Haskell 允許使用特殊語法 (,)
來表示 Pair 構造函數,因此我們可以編寫
pair fa fb = (,) <$> fa <*> fb
但是實際上,我們之前已經看過這種模式 — 這是 liftA2
模式,這使我們開始了整個應用道路。 所以我們可以進一步簡化為
pair fa fb = liftA2 (,) fa fb
但是現在無需顯式寫出函數參數,因此我們可以達到最終的簡化版本:
pair = liftA2 (,)
現在,此函數有什麼作用?當然,這取決於 f
所選的特定對象。讓我們考慮一些特定的例子:
f = Maybe
:如果兩個參數中的任何一個為空,則結果為Nothing
;否則為0
。 如果兩者都是Just
,那麼結果就是Just pairing
f = []
:pair
計算兩個列表的笛卡爾乘積f = ZipList
:pair
與標準zip
函數相同f = IO
:pair
依次執行兩個IO
操作,並返回一對結果f = Parser
:pair
按順序運行兩個解析器(解析器使用輸入的連續部分),並成對返回其結果。 如果任何一個解析器失敗,那麼整個事情就會失敗
可以實現以下函數嗎? 考慮用上述每種類型替換 f
時每個函數的作用
(*>) :: Applicative f => f a -> f b -> f b
mapA :: Applicative f => (a -> f b) -> ([a] -> f [b])
sequenceA :: Applicative f => [f a] -> f [a]
replicateA :: Applicative f => Int -> f a -> f [a]