影片筆記: The Functional Toolkit by Scott Wlaschin

Page content

!! 圖多 !!

!! 以下的圖片都擷取自講者的投影片 !!

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

Conclusion