iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 29
1
Software Development

mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉系列 第 31

mostly:functional 第二十八章:Applicative 的實體

建築跟外觀一樣散發著新穎的氣息,而格局跟上一間也相當類似。而這次大廳中央放著兩座雕塑,兩個之間看起來幾乎是一樣的,只是其中一個比較長一點。而跟之前建築裡的相比,這個兩個上面有著精巧的裝飾。

把四周的的短箭頭丟進去後,從出口跑出來的箭頭,被裹在一球透明的凝膠狀物質裡。我還發現,較長的那個雕塑只能接受較長的箭頭,而短的箭頭只能放進短的雕塑裡。




二元組是一種 Applicative 嗎?

依照二元組的獨特慣例,要證明二元組是 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)

要能夠實做出串列的 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,就能對這個語法是怎麼設計的心領神會。

https://chart.googleapis.com/chart?cht=tx&chl=%5C%7Bx%20%2B%201%20%5Cmid%20x%5Cin%20%5Cmathbb%7BN%7D%5C%7D

串列的 Applicative

於是我們可以討論串列的 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]

所有的 Functor,都是 Applicative 嗎?

不是。例如 Const 這個 functor 就無法證出 applicative 的性質。不過,這又會是另外一個故事了……


自然升格

如果你有注意到的話,我們大多數在 Applicative 的示範,都長成這種格式:

f <$> a <*> b
-- 或是
f <$> a <*> b <*> c

也就是第一步是一個 fmap,接下來才是 <*>。那是因為我們要先把多參數的函式先放進容器裡。而這種第一步先 <$> 的手法,我們稱之為自然升格。因為升格的過程在一般的函式去fmap 容器裡的第一個變數時就被自然的完成了。

顯式升格

自然升格對應的,就是顯式升格了。我們可以手動先用 pure 把函式裝到容器裡,再來進行剩下的 <*>

pure f <*> a <*> b
-- 或是
pure f <*> a <*> b <*> c

LiftA2、LiftA3

不過有時候我們想要直接拿著普通的多參數函式,用多個放在容器裡的參數進行函式應用。而 Haskell 提供了給雙參數函式與三參數函式用的前綴版升格函式:liftA2liftA3

-- 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

那有沒有個單數數函式用的 liftA 呢?有的:

-- Haskell
liftA :: (a -> b) -> f a -> f b

liftA (+1) [1, 2, 3] -- => [2, 3, 4]

「可是,這不就是 fmap 嗎?」

沒錯,這個行為跟 fmap 完全相同。applicative,做為一個比 functor 強的性質,是可以直接由它身上推導出 functor 的實體的。當我們進行升格的函式,只需要一個參數就飽和的情況,就跟 functor 的 fmap 是相同的事情。


你知道這個總是要來的....

函式本身也是一種 Applicative 嗎?

先說結論:是的。

我們之前提過,若把函式本身當成 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 裡面有機會對傳進來的參數偷偷動手腳,而最後再來應用應用了 xh 函式

來看一下實際範例吧:

-- Haskell 
f x, y = (x + 2, y * 3)  -- 我們用 tuple 來示範,比較能看得出來這是*兩個函式並存*的東西
g = (+ 10)

h = f <*> g  -- ap!

h 1 -- => (3,33)

延著這個概念再往下,就會觸碰到 Reader 這個東西。不過我們就先止步於此吧。




而隨著一間間房間走過,那些箭頭不只會改變尾端爪子的樣子。外面原本像是透明凝膠的物質,像是有保護色功能一樣,不,比那個更好。每到一個房間,那個物質就會變得跟該房間裡的東西完全一樣…

在每一間房間裡,只要找到開關的位置,都可以讀到一些文件,而我愈來愈能看懂上面的字了。

走著走著,當然,又是那個放著平台的房間......

[to be contineue…]


上一篇
mostly:functional 第二十七章:Applicative 的法則
下一篇
mostly:functional 第二十八章的試煉: Applicative 的證明
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

尚未有邦友留言

立即登入留言