CWKSC's 翻譯文章

CIS 194 11 Applicative functors, Part II 應用函子,第二部分

Source: 11-applicative2

▌Applicative functors, Part II 應用函子,第二部分

CIS 194 第 11 週

2012 年 4 月 1 日

建議閱讀:

我們首先回顧 FunctorApplicative 類:

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

好,至少具有正確的類型! 但是,不難想像為某種類型創建 FunctorApplicative 實例,但相等性不成立。 由於這將是一個相當可疑的情況,因此我們為這種相等性制定一條法則 —— 這是一種正式的方式來說明給定類型的 FunctorApplicative 實例必須“很好地配合”

現在,讓我們看看更多 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 則不能

在處理 ApplicativeMonad 之類的東西時,請務必記住涉及多個層次的抽象,這一點非常重要。粗略地說,抽像是一種隱藏較低級別細節的東西,它提供了一個“高級”接口,可以(理想情況下)使用該接口,而無需考慮較低級別—儘管較低級別的細節通常會“洩漏”在某些情況下。抽象層的想法很普遍。考慮一下用戶程序-OS-內核-集成電路-門-矽或HTTP-TCP-IP-以太網,或編程語言-字節碼-彙編-機器代碼。如我們所見,Haskell 為我們提供了許多不錯的工具,可在 Haskell 程序本身中構造多層抽象層,即,我們可以向上動態擴展“編程語言”層堆棧。這是一個強大的功能,但可能導致混亂。人們必須學會明確地能夠在多個層次上思考,並在各個層次之間進行切換

特別是對於 ApplicativeMonad,只有兩個級別需要關注。 首先是實現各種 ApplicativeMonad 實例的級別,即 “原始 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 時每個函數的作用

(*>)       :: 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]
CWKSC

Author 作者

CWKSC

喜歡編程,會一點點鋼琴,會一點點畫畫,喜歡使用顏文字 About me 關於我

For any comments or discussions on my blog post, you can open an issue here

對於我博客文章的任何評論或討論,可以在這裏開一個 issue

Feel free to give your comments. OW<