這年頭做為一個開發者,或多或少都有聽過函數式程式設計這個名詞,連過去神祕的Monad概念,也悄悄寄身於主流語言的語法或API之中,因此,探討Monad的文件多了起來,只不過從宿主語言來瞭解Monad,終究會因為它經過調整而感到朦朦朧朧。

若能以主流語言中的Functor、Monad等實現為基礎,進一步認識純函數式語言中的Monad,那麼一切就都會清晰可見了。

從Functor Typeclass開始

Monad是一組行為,Monad的實現封裝了某個可共用的運算情境,而開發者可以指定特定值來重用這些情境,我在先前專欄〈神祕的Monad不神祕〉〈尋找野生的Monad〉中,已經舉了不少主流語言中的實例,理解這些相關語法或API,對於流程重用性的敏感度會是非常好的訓練,如果目標轉向了純函數式語言如Haskell,想瞭解更純粹的Monad是怎麼一回事,實際上並不容易,然而,可以從比較簡單的Functor Typeclass開始。

Haskell中的Typeclass,某種程度上就類似Java中的interface,規範一組應當實現的行為,Functor Typeclass規範了要將某型態對應(map)至另一型態的行為fmap,它的函式型態是(a -> b) -> f a -> f b,乍看不容易理解,不過,講到對應,就會想到List的map,如果執行map chr [65, 66, 67],那麼結果是['A', 'B', 'C'],chr的型態是(Int -> Char),[65, 66, 67]型態是[Int],['A', 'B', 'C']型態是[Char],對照fmap的函式型態,a就是Int、b就是Char,那麼f就是[]這個值建構式了。

實際上,List的map函式的確實現了Functor的行為,因此,也可以使用fmap chr [65, 66, 67]來得到['A', 'B', 'C'],map的實現,如許多開發者已知的,封裝了走訪List的流程,然而,Functor的fmap則是進一步地,定義了map行為的抽象,那麼,還有什麼型態實現了Functor的fmap?從Maybe來看,也許最容易理解。

現今開發者對Maybe應該也不陌生,不少語言中都內建有類似語法或API,像是Java 8中的Optional,它有個map方法,例如對於Optional<String>的nickName,可以執行nickName.map(name -> name.length())傳回Optional<Integer>。

類似地,對於Haskell的Maybe String,可以執行fmap length (Just "justin")來傳回Maybe Int,兩者都重用了有無值的判斷流程,也就是說,Maybe同樣實現了Functor的行為。

Applicative是加強版的Functor

在使用Maybe這類的Functor時,經常會遇到一個麻煩,例如分別有Just 10與Just 3,沒辦法(Just 10) * (Just 3),得分別取得Just中的10與3才能進行相乘,如果不知道有Applicative,就會寫出許多模式比對取值的重複流程,如果知道有Applicative,就可以直接寫下(*) <$> Just 10 <*> Just 3直接得到Just 30,無需自行撰寫取值而後套用函式的過程。

Applicative的定義是class (Functor f) => Applicative f,以Java的interface來類比的話,就像是有個Applicative介面繼承了Functor的介面,因此可說是Functor的加強版,其中,規範的pure函式,用來將指定值裝入Applicative之中,以便後續可套用<*>函式的運算情境。

這感覺就像是將值置於Maybe之中,以便後續可以套用fmap函式的運算情境;實際上,Maybe就實現了Applicative的行為,因此,pure (*) <*> Just 10 <*> Just 3也會得到Just 30的結果。

對於Maybe來說,<*>是基於fmap來實現,方才介紹Functor中可以看到,fmap接受一個函式與Maybe,然後,對Maybe中的值套用函式而得到另一個Maybe,而<*>則是接受一個裝有函式的Maybe與另一個裝有值的Maybe,然後分別取得Maybe中的函式與值進行套用,而得到另一個Maybe。因此,<*>可說是fmap的加強版。在scalaz這個專案中,你若有試著實現Applicative的概念,可以使用^(3.some, 5.some) {_ + _}來取得Some(8)的結果。

在Haskell中,List也實現了Applicative,對於[x * y | x <- [1, 2, 3], y <- [4, 5, 6]],可以使用(*) <$> [2, 5, 10] <*> [8, 10, 11]來得到相同的結果,簡單來說,就是隱藏了分別走訪兩個List的運算情境,讓開發者可以對兩個List指定函式進行運算,傳回新的List。

也是將值置入運算情境的Monad

Functor規範了將值建構式中的值對應至另一值的行為,實際上使用像Maybe這類的值建構式,就是將值置入Functor的運算情境;Applicative的pure,只是將值置入運算情境這樣的行為,明確定義出來,Applicative的實例在實現pure時,其實是在隱藏值建構式的行為,至於fmap與<*>,從函式型態分別為(a -> b) -> f a -> f b 與f (a -> b) -> f a -> f b可以看出來,都是規範必須能讓使用者指定如何進行對應的函式,從一個運算情境(f a),轉換至另一個運算情境(f b)。

Monad其實也是相同的概念,Haskell中,Monad是個Typeclass,它有個return規範了a -> m a,這就相當於Applicative的pure規範了a -> f a,只是型態參數命名不同,另一個>>=規範了能讓開發者指定如何進行對應的函式,這像是Applicative的<*>,只不過>>=的型態是m a -> (a -> m b) -> m b,也就是可接受前一個運算情境(m a)中的值(a),運算後傳回另一個包括結果(b)的運算情境(m b)。

對Maybe來說,return的實作就是Just值建構式,因此,對於〈神祕的Monad不神祕〉一文中,我第一個示範Optional的flatMap程式碼來說,若有個Haskell的customer函式,可接受Order,並傳回Maybe Customer,代表資料庫中可能存在或不存在客戶資料,以及address函式可接受Customer,並傳回Maybe Address,代表資料庫中可能存在或不存在寄件位址資訊,那麼,使用Haskell寫出來的對照程式碼,就會像是(return order) >>= customer >>= address,從而避免了巢狀的有無結果比對流程。

至於List,return實作就是[]值建構式,因此,對於〈神祕的Monad不神祕〉中示範List的flatMap程式碼來說,若有個Haskell的lineItems函式可接受Order並傳回[LineItem],有個premiums函式可接受LineItem並傳回[Premium],那麼使用Haskell寫出來的對照程式碼,就會像是orders >>= lineItems >>= premiums,一整個清楚易懂,不用涉入List的走訪等細節。

繼續探索純函數式的世界

函數式程式設計無疑地,已歷經時代的考驗,並紛紛以特定(簡化)的樣貌進入到主流語言之中,讓開發者可以從更具體的角度,吸收、思考並應用函數式典範中的高階抽象概念,這也開放了進入純函數式世界的一扇門,因為,有了從函數式元素獲益的經驗,開發者會更樂意探索更多純粹的函數式元素。

只是在探索純函數式元素時,難免仍會因為接觸更多的抽象而理解不易。

就像我首次看到Functor Typeclass的定義時,意識就開始神遊一般,然而,自己對於Optional的使用經驗,以及其與Maybe間的關係,幫了個大忙,從而聯想到Optional的map與Fuctor的fmap之間的關係,這也使得後續對Applicative與Monad的探索,得以繼續展開。

因此,如果還沒接觸過主流語言中的函數式元素,可以試著積極地去瞭解它們,因為,函數式的典範確實已經在影響實務程式開發。

若已經對主流語言中的函數式元素有一定的使用經驗,可試著繼續探索純函數式的世界中,這些元素會是什麼的樣貌,此時,過去運用函數式元素的經驗會有所助益,而對這些函數式元素,也將不再會有朦朧之感。

作者簡介


Advertisement

更多 iThome相關內容