影片筆記: The Functional Toolkit by Scott Wlaschin
!! 圖多 !!
!! 以下的圖片都擷取自講者的投影片 !!
https://www.slideshare.net/ScottWlaschin/the-functional-programming-toolkit-ndc-oslo-2019-150648710
前言
會覺得 functional programming 很可怕,也許我們只是不熟悉這些在 FP 的字而已
如果把
- functor 換成 mappable
- catamorphism 換成 collapsable
- monad 換成 chainable
- monoid 換成 aggregatable
就變得平易近人一些了 (?)
而且事實上,object oriented programming 也有一些看起來很可怕的字 w
The Functional Kit
這個影片主要在說 working with effects 的這幾個部分,也就是 functor, monad 和 applicative
Part 1. Core principles of statically-typed FP
Functions are things
function 是把輸入轉換成輸出的東西
function 是獨立的,不屬於任何 class,因為如此,function 是 reusable 的
function 可以是輸入、輸出甚至參數
?? 這邊我就不懂輸入和參數差在哪 (p.22, 23)
Composition everywhere
講者把 function 比喻成積木
- 每一塊都是設計成可以被組合的
- 我們可以組合兩個積木,讓他成為一塊大的積木,然後把這塊新的積木又用在其他地方
- 這些積木在許多情況下都是 reusable 的
function 也是如此
我們可以用這些小積木組合成很大的東西、系統
那麼這些概念要如何套用在 programming 上
Function composition
Web application 本質上也是 function,輸入是 http request,輸出也是 http request。而在這中間經過了許多 function 的作用。
講者有別的影片專門在講 The Power of Composition
F#: Composition example
在 F# 中,用 let 定義 值 或 函數 ; 用 fun 來定義 lambda function
let add1 x = x + 1
let double x = x + x
let square= x -> x*x
太多層的 nested function 很難懂
square (double (add1 5)) // = 144
在 F# 提供了 piping 的形式,想像資料一路流過去
5 |> add1 |> double |> square // = 144
Problems with composition
如果輸入和輸出沒有對上
例如左上 function 的輸出是香蕉,右下 function 的輸入是櫻桃,兩者對不上
這時候我們需要一個放在中間,負責轉換的函數,把前一個function 的輸出 – 香蕉,先換成下一個 function 的輸入 – 櫻桃。
F# 例子
intToStr 在這裡幫我們把兩邊的 type 對上
5 |> add1 |> strLen
// ^ error: expecting an int
5 |> add1 |> intToStr |> strLen
// ^ fixed!
那如果 type 有對上但他被包在其他東西裡面,例如 Option<香蕉> 或 List<香蕉> ?
劇透一下,後面會提到,Monads !!
Part 2. Function Transformers
我現在想要更新我的名字和電子郵件信箱,server 端要做的事情如下
寫成 code 會長這樣
不過這樣挺危險的,執行過程中可能會出錯。如果把上面的 code 加上 error handling
code 變得好醜、很亂 OAO,有沒有辦法讓他保持一開始的簡潔但仍然有 error handling
Use a Result type for error handling
type Result =
| Ok of SuccessValue
| Error of ErrorValue
在 Haskell 中應該會中應該會用 Either a b
來做
一個輸入,但有兩種可能的輸出,一個是成功的,另一個是失敗的
講者這裡用鐵道模型來比喻,個人覺得滿生動的 !
如果是很多軌道串在一起,會長成這樣
關於這個詳細的說明在另一部影片裡 Railway Oriented Programming
講者有另一篇文,並不是所有情況都適合這樣的設計 !! against-railway-oriented-programming
Implement Railway Oriented Programming
如果這些 function 都是 2-track 的,很好,很自然就可以串在一起
但是,如果不是,要如何組合 mismatched 的 functions ?
“Bind” is the answer !
Bind all the things !
想辦法透過一個中間的函數 bind 從 1-track in, 2-track out 轉換成 2-track in, 2-track out
透過 bind,我們就可以把這些 function 轉換後串在一起
validation using bind
用 bind 把他升格成 2-track in, 2 track out
在 F# 裡,用 pipe 的寫法
我們接下來也可以把這一整塊再當成一個大的 function
Shapes vs. Types
有些 function 不會產生 error,他就是 1-track in, 1-track out 的,例如 lowercaseEmail 只是把輸入的 Email 字串轉成小寫,不會有出錯的機會
我們還是可以把它轉換成 2-track 的,如果進來是 OK s,那就做原本不會出錯的那個函數,而如果進來是 Error,那就直接把 Error 傳出去
透過 function transformers,我們可以任意組合不同形狀的函數
講者的鐵道模型滿酷的 !!
Part 3. Understanding “effects”
這裡要開始介紹一個大主題 – effects 了
下面主要會講 List, Option, Async
“Normal” world vs. “Effects” world
Generic “Effects” World
不同的名字,但說的概念差不多
- “Effect” world
- “Enhanced” world
- “Elevated” world
因為都是 E 開頭的字,所以他取 E<string> 的寫法
Challenge: How to work with effects?
Example: Working with Options
- 第一種情況
有時候我們的函數是從 real world 轉換到 option world,但我們要取包在 option 裡面的值,所以又把他轉到 real world,但之後的函數可能又是轉到 option world,以此類推,就這樣在不同 world 之間轉換來、轉換去,但這不是一個好的模式
- 第二種情況
我們希望當他轉換到某個 effect world 之後就待在那,在 effect world 之間轉換,等到最後結束,再轉回 real world
這就是上面的第一種情況:拆開來,套用函數,再包回去,在兩個世界中盪來盪去
那要怎麼做到上述的第二種情況,讓他轉換到 effect world 後就待在那邊轉換
Tool #1 Moving functions between worlds with “map”
這是對一個 Option 的值加一,做的事情就是拆開來,如果是 Something 就做加一,None 的話就 None
現在我們把 add1 當成參數傳進去,換成 f
換用 lambda 函數的方式寫
輸入是 real world 的 function,但現在結果是 Option -> Option 的 function
我們把輸入輸出都在 real world 的函數,轉換成輸入輸出都在 effect world 的函數
Example: Working with List world
這裡做的事情就和上面 option world 的例子差不多
把操作變成參數,從輸入輸出在 real world 的函數轉換成輸入輸出都在 list world 的函數
Guideline: Most wrapped generic types have a “map”. Use it!
上面說的從 real world 轉換到另一個世界,effect world,在 Haskell 中指的就是 Functor
Functor 需要實作 fmap
fmap :: (a -> b) -> f a -> f b
如果換個方式看
fmap :: (a -> b) -> ( f a -> f b )
就是從一個 a -> b 的函數,轉換成 f a -> f b 的函數
Haskell 中的 Either, [ ] (List), Maybe 等都屬於 Functor !
Guideline: If you create your own generic type, create a “map” for it.
FP terminology: functor
Tool #2 Moving values between worlds with “return”
這點滿直觀的
從 real world 到 option world
x 是 42,直接包在 Option 裡,轉換到 option world
let x = 42
let intOption = Some x
從 real world 到 list world
x 是 42,直接包在 List 裡,轉換到 list world
let x = 42
let intList = [x]
Tool #3 Chaining world-crossing functions with “bind”
world-crossing functions 是像這樣的函數,輸入輸出在不同的世界
let range max = [1..max]
// int -> List<int>
let getCustomer id =
if customerFound then
Some customerData
else
None
// CustomerId -> Option<CustomerData>
Problem: How do you chain world-crossing functions?
最直接的方式就是從最外層開始,如果是 Something,就撥開來,一層一層往下,如果是遇到 None 就直接 None
但這樣 code 變得很亂
我們可以從 code 中看出一些固定的模式,他的模式都是
if z.IsSome then
// do something with z.Value
// in this block
else
None
這裡把 x, y, z 抽出來,也把操作 f 抽出來當參數,可以把模式歸納成
let ifSomeDo f opt =
if opt.IsSome then
f opt.Value
else
None
現在 code 可以寫成這樣,好讀很多
let example input =
doSomething input
|> ifSomeDo doSomethingElse
|> ifSomeDo doAThirdThing
|> ifSomeDo ...
和前面提到的鐵道模型一樣 !
Pattern: Use “bind” to chain tasks
a.k.a “promise” “future”
future 和 promise 的模式和上面 option 的例子很像
他們都有一個固定的模式,在這裡是,當這件事完成了,就接著做下面另一件事 ; 但如果失敗了,就跳過去出來了 (option 例子的 None)
Why is bind so important ?
It makes world-crossing functions composable
bind 讓我們可以組合 world-crossing functions
從 a -> E<b> 轉換成 E<a> -> E<b>
如果都是 a -> E<b> 這種對角的 function 無法組合
但如果把這些 function 一一都換成平行的(都在 effect world) 的函數,就可以很輕鬆地組合在一起了 !
FP terminology: monad
TLDR: If you want to chain effects-generating functions in series, use a Monad
Notes. in Haskell, IO Monad
Tool #4 Combining effects in parallel with applicatives
Options
Lists
The general term for this is “applicative functor” Option, List, Async are all applicatives
FP terminology: applicative
Problem: How to validate multiple fields in parallel?
如果是在 real world,我們都知道如何把資料包在一起,就寫一個 validCustomer 的 type,用 constructor 把 Name, Email 和 Date 包在一起
但在 Result world 呢 ?
接下來要介紹 liftA2, liftA3, …
就像 map 一樣,都是從 real world 轉換到另一個 effect world,但不一樣的是,liftA2 有 2 個參數
Prelude> :m Control.Applicative
Prelude Control.Applicative> :i liftA2
class Functor f => Applicative (f :: * -> *) where
...
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
...
-- Defined in ‘GHC.Base’
而 liftA3 則是有 3 個參數
現在 code 會變成
我們把 makeCustomer 這個在 normal world 的 constructor function 升格成在 effect world 也適用
我覺得這樣看比較清楚
-- liftA3 的 type signature
(a -> b -> c -> d) -> f a -> f b -> f c -> f d
-- 幫他加一些括號
-- 輸入是 function: 三個輸入 a, b, c,輸出是 d
-- 輸出是另一個 function: 一樣是三個輸入 f a, f b, f c, 輸出是 f d
( (a -> b -> c) -> d ) ->
( (f a -> f b -> f c) -> f d )
我的整理
對照一下 Haskell 中的對應
圖中由左至右,分別對應下面由上而下
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
class Applicative m => Monad (m :: * -> *) where
return :: a -> m a
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
-- 要 import Control.Applicative
class Functor f => Applicative (f :: * -> *) where
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
Part 4. Using all the tools together
下面是個綜合例子
- Download a URL into a JSON object
- Decode the JSON into a Customer DTO
- Convert the DTO into a valid Customer
- Store the Customer in a database
影片從 #t=59m44s 開始,投影片則是 p.238 開始