建築跟外觀一樣散發著新穎的氣息,而格局跟上一間也相當類似。而這次大廳中央放著兩座雕塑,兩個之間看起來幾乎是一樣的,只是其中一個比較長一點。而跟之前建築裡的相比,這個兩個上面有著精巧的裝飾。
把四周的的短箭頭丟進去後,從出口跑出來的箭頭,被裹在一球透明的凝膠狀物質裡。我還發現,較長的那個雕塑只能接受較長的箭頭,而短的箭頭只能放進短的雕塑裡。
依照二元組的獨特慣例,要證明二元組是 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 * 2 | x <- xs] -- => [2, 4, 6]
---
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…]