iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 27
1
Software Development

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

mostly:functional 第二十六章:升格,再一次升格,然後再…

在後面幾個比較深的房間裡,我訝異的發現,那些短箭頭,還能夠進化成更加特別的形狀…




const

在 Monoid 的章節裡,我們看過了 id 這個什麼事都不做的函式。而現在要介紹一個差不多奇怪的函式,叫做 const。這個函式接收兩個參數,然後原封不動的回傳第一個參數。還有,const 是一個懶惰的函式。

-- Haskell 語法
const :: a -> b -> a
const a b = a -- 已內建,只是秀出來讓你們羨慕一下

-- 使用
toOne = const 1

-- 呼叫
toOne "test" -- => 1

如果用其它語言,如 JavaScript ,就得要自己實作const 了,像是這樣:

// JavaScript 語法
let const = a => b => a //寫出來之後,發現好像也沒有多值得羨慕了

// 試試看
let toOne = const(1)

toOne(100) //=> 1

或 Elixir

# Elixir 語法
const = fn a -> fn b -> a end end

# 試試看
to_one = const.(1)

to_one.(999) #=> 1

這個函式乍見跟 id 一樣沒啥路用。但是它的其中一個功能,就是來做為無視輸入,進行置換用的函式。而我們要用這個函式與 fmap 搭配,做出魔法般的事。

Functor 的秀異之處:連續升格

我們一直說要把容器抽象來看這件事,在此終於可以體現其真正的優勢了。我們先來假設一個多層的資料結構:

-- Haskell 語法
things = [
  Just "hello world",
  Nothing,
  Just "Haskell",
  Just "Elixir"
]

雖然看似簡單,但是當我們帶上 functor 的視角來看時,這是一個三層的 functor。第一層、也就是最外層是個串列,第二層是裡面一個個的 Maybe String,而第三層就是 Just 裡面的那個 String 了。串列、Maybe 跟 String 都是 functor 的,記得嗎?

接下來,我們再來做一個無視輸入是什麼,都會回傳一個字元 'A' 的函式。

-- Haskell 語法
toA = const 'A'

It's ...Showtime!

  1. 把整個串列代換掉
toA things -- => 'A'
  1. 升格,改動串列內容,字母的串列會被印成字串
fmap toA things 
-- => "AAAA"
  1. 升格兩次,改動 Maybe 的內容
(fmap . fmap) toA things 
-- => [Just 'A', Nothing, Just 'A', Just 'A']
  1. 升格三次,改動字串的內容
(fmap . fmap . fmap) toA things
-- => [Just "AAAAAAAAAAA", Nothing, Just "AAAAAAA", Just "AAAAAA"]

型別

再來看一下 fmap 及其組合們的型別:

-- Haskell 語法

fmap :: Functor f => 
     (a -> b) -> f a -> f b

(fmap . fmap)
  :: (Functor f1, Functor f2) => 
     (a -> b) -> f1 (f2 a) -> f1 (f2 b)

(fmap . fmap . fmap)
  :: (Functor f1, Functor f2, Functor f3) =>
     (a -> b) -> f1 (f2 (f3 a)) -> f1 (f2 (f3 b))

當然,一如往常,我們還是要問最後一個問題:

函式是一種 Functor 嗎?

我們之前一直提到,函式是一種計算過程的容器,就是預備著當我們走到這裡時,可以啟發看到這個本質的洞見

舉例來說,當我們有一個 \a -> a * 3 + 1 的函式時,我們可以把它看成一個裝在 a -> 的容器中,元素是 a * 3 + 1,只是這個值是一種計算過程。

把這個抽象化來看,不管那個計算有多複雜,當我們有一個 a -> b 的函式時,用容器的角度來看,這是一個裝載在 a -> 容器裡的 b 的值。當然就像上面所說的,這個 b 會是個計算,簡單的或複雜的都沒差。

那當我們用把一個函式視為容器,對它進行 fmap,例如我們傳了 f 這個函式當做第一個參數。那麼依照 functor 那個保留原來的外殼,而用裡面的值呼叫函式這個行為,那麼會變成這樣:

-- Haskell
fmap f (\a -> b)
-- 會變成
\a -> f(b)

如果你退後一點看,你會發現,這就是把原先計算的結果,再傳遞給…f 這個函式。

是的,用 f 函式去 famp 另一個函式 g,其行為就跟用 fg 進行函式組合是一模一樣的。

-- Haskell 語法

fmap f g
--- 等同於
f . g

讓我們把 fmap 作用在函式上的型別限制寫出來,雖然意思跟函式組合是相同的,這個型別標註,更能看出來函式是一種容器的特性:

-- Haskell 語法
(<$>) @((->) _) :: (a -> b) -> (_ -> a) -> _ -> b

-- 把最後兩個括起來會更加明顯:
(<$>) @((->) _) :: (a -> b) -> (_ -> a) -> (_ -> b)

-- 跟函式組合(最後的部份加上括號)對比
(.)             :: (b -> c) -> (a -> b) -> (a -> c)

驗證

我們能夠驗證,函式的 functor 實體,也能夠符合那三條法則嗎?

-- Haskell 語法
f = (+1)
g = (*2)
h = (+3)

-- 封閉律與單位元素
fmp = fmap id f
fid = id f

f 1   -- => 2
fmp 1  -- => 2
fid 1 -- => 2

-- 分配律
hc = fmap (f . g) h
hd = fmap f . fmap g $ h

hc 1 -- => 9
hd 1 -- => 9

胡亂搞一通

既然我們可以對函式 fmap,而 fmap 本身也是一個…嗯…函式,那我們可不可以對 fmap 進行 fmap?當然!

fmap f fmap
-- 等同於
f . fmap
fmap fmap fmap -- 嘿!?
-- 等同於
fmap . fmap
fmap fmap $ fmap fmap fmap -- 住、住手!
-- 等同於
fmap . fmap . fmap

又來個沒什麼用的?

我們知道了 <> 用在函式上等同於. 進行函式組合。我們現在又瞭解 <$> 也是函式組合,差別只在它不限制回傳值的型需要是一樣的 Monoid。雖然依然感覺一點用都沒有,相當空虛,不過為了理解接下來的型別,這兩件事其實很重要




雖然外表上看得出來,但在無數次來回這世界,一間間的房間探索與試錯之後,才深刻的體會到,這一棟建築怎麼會大成這個樣子?就在我以為可能永遠都走不到盡頭的時候,就打開了那擺著平台的房間的門。我深呼吸了幾次,才向著房間裡面走去……

[to be continue]


上一篇
mostly:functional 第二十五章:Functor 的實體
下一篇
mostly:functional 第二十六章的試煉:Functor 的證明
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

尚未有邦友留言

立即登入留言