iT邦幫忙

2021 iThome 鐵人賽

DAY 22
0
Software Development

Functional Programming For Everyone系列 第 22

Day 22 - IO Monad

上一章節簡單的介紹如何處理 side effect 的其中一個方法 dependency injection,而本章要介紹第二個方法 IO Monad,但在這之前要先了解什麼是 thunk!

Thunk

In computer programming, a thunk is a subroutine used to inject a calculation into another subroutine. Thunks are primarily used to delay a calculation until its result is needed, or to insert operations at the beginning or end of the other subroutine. They have many other applications in compiler code generation and modular programming. -wiki

簡單來說, thunk 就是一個函式其包覆一個表示式,其目的就是為了延遲運算。

thunk :: () -> a

而延遲運算跟 side effect 有什麼關係呢?

example

const secret = () => {
    console.log('hi, :))))');
    return 18;
}

現在有一個 secret 的函式,可以看到這個函式是 impure function,因為在呼叫的同時,也在 console 寫入一些東西,但又必須知道這個 secret 內的值才能進行接續的動作,有什麼方式讓 secret 既是 pure function 也可以在呼叫後接續其他動作

統整一下我們現在要做的事

  1. 改寫 secret, 從 impure 到 pure function
  2. 用 secret number 繼續進行運算

改寫 secret, 從 impure 到 pure function

沒錯,這時候 thunk 的概念就派上用場了

const secretThunk = () => {
    const secret = () => {
        console.log('hi, :))))');
        return 18;
    }
    return secret;
}

現在 secretThunk 已經是一個純函式了,給定一樣的輸入就會回傳一樣的輸出,並且無任何 side effect.

secretThunk() // f
secretThunk() // f
secretThunk() // f

用 secret number 繼續進行運算

如果現在我們得到了 secret number,還想要再進行其他動作呢? 例如將其乘 2

const calcSecretNumber = (f) => {
    return f() * 2;
}

calcSecretNumber(secret)

// hi, :))))
// 36

但如果還有其他動作呢?? 這樣不就產生了 side effect 了嗎? 所以也用 thunk 包住其運算

const calcSecretNumberThunk = (f) => {
    return () => f() * 2;;
}

const doubleSecret = calcSecretNumberThunk(secret)
const quadrupleSecret = calcSecretNumberThunk(doubleSecret)

quadrupleSecret()

可以看到我們將 side effect 的程式碼,透過 thunk 這個概念,將其延遲執行,直到需要時在呼叫。這就像是劃分了一條界線,將 impure 跟 pure 切分開來,我們無法避免 side effect,但可以將其封裝在一個類似盒子裡,並在需要的時候在打開,而這個概念就延伸出 IO Monad, 但我們總不會想要每次都要將 impure function 再包一層,讓其變成 pure,或是把每個相關用到的函式都套用 thunk。

所以來介紹 IO Monad 吧!

IO Monad

Constructor

IO :: () -> a

沒錯 IO Monad 的 type signature 跟 thunk 基本上概念是一樣的,就是包覆住該表示式,而剛剛提到要如何再取得 secret number 後運算,其實就是 IO functor 在做的事

Functor

const IO = run => ({
    run, 
    map: (f) => IO(() => f(run()))
})

IO.of = (x) => IO(() => x);

改寫前述範例

const secret = () => {
  console.log('hi, :))))');
  return 18;
};

const calcSecretNumber = (num) => num * 2;

const effect = IO(secret)
    .map(calcSecretNumber)
    .map(calcSecretNumber)


effect.run() 
// hi, :))))
// 72

可以看到 calcSecretNumber 就是一般函式,不用在進行任何加工,就如同前面所提到的,當需要執行這個 side effect 的程式時,只需要呼叫 run 就好,而在這之前此程式都不會有任何動作。

Chain

如果今天是 effect 跟 effect 要進行 compose 呢?? IO Monad 的 chain 該如何實作

const IO = run => ({
    run, 
    map: (f) => IO(() => f(run())),
    chain: (f) => IO(() => f(run()).run()),
})

而 IO 的 chain method 其實也很簡單,邏輯跟 map 基本上是一樣的,只不過多了打平,像是要將 IO(IO(a)) 打平,方法就是呼叫一次 run,這樣結構就會變成 IO(a)

舉例,現在有另一個 IO otherEffect

const otherEffect = (num) => IO.of(R.add(10, num));

此時要將兩個 IO 做 compose,就可以用 chain

const effect = IO(secret)
  .map(calcSecretNumber)
  .map(calcSecretNumber)
  .chain(otherEffect);
 
effect.run()

// hi, :))))
// 82

Applicative Functor

const IO = run => ({
    run, 
    map: (f) => IO(() => f(run())),
    ap: eff => eff.map(effRun => effRun(run())),
    chain: (f) => IO(() => f(run()).run()),
})

const lift2 = R.curry((g, f1, f2) => f2.ap(f1.map(g)))

ap 也不用多加贅述,想必各位讀者都非常熟悉了! 也就是將兩個以上的 IO 跟函式進行結合!

const result = lift2(R.add, IO.of(1), IO.of(1)) 

result.run() // 2

point-free version

以下提供 point-free version 給大家參考

IO Monad

const of = a => () => a;
const map = (run) => (fb) => () => run(fb())
const chain = (run) => (mb) => () => run(mb())() 
const ap = (run) => (f2) => () => {
    const f = f2();
    const a = run();
    return f(a)
} 

example

const otherEffect = (num) => of(R.add(10, num));

const pipe = (init, ...fns) => 
  fns.reduce((prevValue, fn) => fn(prevValue), init);
  
const effect = pipe(
  secret,
  map(calcSecretNumber),
  map(calcSecretNumber),
  chain(otherEffect)
);

effect();
// hi, :))))
// 82

小結

之後再實作篇,也會有 IO Monad 的實際案例,而今天就是帶讀者們了解概念!

感謝大家的閱讀 /images/emoticon/emoticon07.gif

NEXT: Either Monad

Reference

  1. IO Moand
  2. FP Book

上一篇
Day 21 - Handle Side Effect I
下一篇
Day 23 - Either Monad
系列文
Functional Programming For Everyone30

尚未有邦友留言

立即登入留言