建築跟外觀一樣散發著新穎的氣息,而格局跟上一間也相當類似。而這次大廳中央放著兩座雕塑,兩個之間看起來幾乎是一樣的,只是其中一個比較長一點。而跟之前建築裡的相比,這個兩個上面有著精巧的裝飾。
把四周的的短箭頭丟進去後,從出口跑出來的箭頭,被裹在一球透明的凝膠狀物質裡。我還發現,較長的那個雕塑只能接受較長的箭頭,而短的箭頭只能放進短的雕塑裡。
依照二元組的獨特慣例,要證明二元組是 applicative 也需要一個前題。這個前題可以讓我們更能體會上一節所說,「Applicative 有隱含的 monoid 特質」這件事。
二元組是 Applicative 的前題,就是前面的元素,必須要是 Monoid 才行。當你看到 (a, b) 這個二元組的時候,你可以把它當成裝在 (a,) 容器裡的 b。
那麼在進行 ap 的時後,裡面是函式應用,那外面的殼,包含那個 a,就要進行 mappend 了。
而另一個函式 pure,把輸入的值放在後面沒問題。那前面呢?既然那個位置是個 monoid,我們只要放個 mempty 在那邊,編譯時再來進行推導就可以了。
instance Applicative ((,) a) where
pure b = (mempty, b)
(a, f) <*> (a', b) = (mappend a b, f b)
-- 試試看
(Sum 3, (*10)) <*> (Sum 4, 20) -- => (Sum 7, 200)
pure (+8) <*> ("hello", 10) -- => ("hello", 18)
是的。但是為了寫出它的實作,我們還得多介紹一樣東西:
要能夠實做出串列的 applicative,得要先說明一下串列本身具有的一個功能:串列推導 (list comprehension)。如果用過 python 或是 elixir 的人,應該蠻習慣這個的了。而對於不熟悉這個語法的人,只要知道這個有點類似能產出裝在串列裡的元素的 for 迴圈,而且可以做出巢狀(多層)迴圈做的事就可以了。
-- Hakell 語法
xs = [1, 2, 3]
[x + 1 | x <- xs] -- => [2, 3, 4]
---
ys = ['a', 'b']
[(x, y) | x <- xs, y <- ys]
-- => [(1,'a'),(1,'b'),(2,'a'),(2,'b'),(3,'a'),(3,'b')]
對比數學上的 set notation,就能對這個語法是怎麼設計的心領神會。
於是我們可以討論串列的 applicative 了,先來看一下它會怎麼運作:
-- Haskell 語法
(+) <$> [1, 2] <*> [3, 4, 5] -- => [4,5,6,5,6,7]
如果有點看不懂的話,可以想成第一次的 fmap 先產生裝在串列裡的 [(+1), (+2)]兩個函式。
接著用 (+1) <$> [3, 4, 5], 再用 (+2) <$> [3, 4, 5] ,接著把兩個結果串列結合在一起。
這個是怎麼實作的呢?
-- Haskell
instance Applicative [] where
pure = []
fs <*> xs = [f(x) | f <- fs, x <- xs]
不是。例如 Const 這個 functor 就無法證出 applicative 的性質。不過,這又會是另外一個故事了……
如果你有注意到的話,我們大多數在 Applicative 的示範,都長成這種格式:
f <$> a <*> b
-- 或是
f <$> a <*> b <*> c
也就是第一步是一個 fmap,接下來才是 <*>。那是因為我們要先把多參數的函式先放進容器裡。而這種第一步先 <$> 的手法,我們稱之為自然升格。因為升格的過程在一般的函式去fmap 容器裡的第一個變數時就被自然的完成了。
與自然升格對應的,就是顯式升格了。我們可以手動先用 pure 把函式裝到容器裡,再來進行剩下的 <*>:
pure f <*> a <*> b
-- 或是
pure f <*> a <*> b <*> c
不過有時候我們想要直接拿著普通的多參數函式,用多個放在容器裡的參數進行函式應用。而 Haskell 提供了給雙參數函式與三參數函式用的前綴版升格函式:liftA2 與 liftA3:
-- Haskell 語法
liftA2 (+) [1, 2] [3, 4, 5] -- => [4,5,6,5,6,7]
liftA3 (,,) "ab" "xy" [1, 2]
-- => [('a','x',1),('a','x',2),('a','y',1),('a','y',2),('b','x',1),('b','x',2),('b','y',1),('b','y',2)]
那有沒有個單數數函式用的 liftA 呢?有的:
-- Haskell
liftA :: (a -> b) -> f a -> f b
liftA (+1) [1, 2, 3] -- => [2, 3, 4]
「可是,這不就是 fmap 嗎?」
沒錯,這個行為跟 fmap 完全相同。applicative,做為一個比 functor 強的性質,是可以直接由它身上推導出 functor 的實體的。當我們進行升格的函式,只需要一個參數就飽和的情況,就跟 functor 的 fmap 是相同的事情。
你知道這個總是要來的....
先說結論:是的。
我們之前提過,若把函式本身當成 functor,用另一個函式對它 fmap,則會用傳入的函式變動其輸出值。而若我們把函式當成 Applicative,那麼可以做出把同一個參數同時傳給多個函式的東西。
我們先來比對一下泛用的 applicative 的兩個函式型別定義,以及當把容器代換成函式的型別定義:
-- Haskell 語法
-- 把任意的 functor, f,用裝在函式容器裡的東西來思考,也就是 (r -> )
pure :: a -> f a -- 任意 functor 的 pure
pure :: a -> (r -> a) -- 函式上的 pure
(<*>) :: f (a -> b) -- 任意 functor 的 ap
-> f a
-> f b
(<*>) :: (r -> a -> b) -- 函式上的 ap
-> (r -> a)
-> (r -> b)
instance Applicative ((->) a) where
pure = const
g <*> h = \x -> g x (h x)
pure 一開始可能比較不好想像。那就先回到定義:給它一個值,回傳包在容器裡的這個值。如果用函式來代替上一句的容器的話,那我們就是要給它一個值,拿到一個直接回傳這個值的函式。嗯?有沒有覺得很熟悉?我們之前有做過這種沒路用的東西:const,當時我們還拿它來做為連續升格的範例。
而 <*> 的部份,第一個參數是接收兩個參數的函式,第二個參數也是個函式,而回傳值當然也是個函式。這裡有趣的地方在於 g 是一個雙參數函式,而 h 是個單參數函式,先用 g 部份應用了 x,代表我們在 g 裡面有機會對傳進來的參數偷偷動手腳,而最後再來應用應用了 x 的 h 函式。
來看一下實際範例吧:
-- Haskell
f x, y = (x + 2, y * 3) -- 我們用 tuple 來示範,比較能看得出來這是*兩個函式並存*的東西
g = (+ 10)
h = f <*> g -- ap!
h 1 -- => (3,33)
延著這個概念再往下,就會觸碰到 Reader 這個東西。不過我們就先止步於此吧。
而隨著一間間房間走過,那些箭頭不只會改變尾端爪子的樣子。外面原本像是透明凝膠的物質,像是有保護色功能一樣,不,比那個更好。每到一個房間,那個物質就會變得跟該房間裡的東西完全一樣…
在每一間房間裡,只要找到開關的位置,都可以讀到一些文件,而我愈來愈能看懂上面的字了。
走著走著,當然,又是那個放著平台的房間......
[to be contineue…]