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

<<:  [生日优惠-3] 汉来海港餐厅Buffet #当日寿星6折

>>:  【没钱买ps,PyQt自己写】Day 22 - PyQt 视窗的个性化/属性控制 setWindowFlags,禁止放大缩小、永远显示於最上层/最下层

Day 21 BeautifulSoup模组三

今天的影片内容为介绍分析项目清单与表格文件的方法 而在影片的後半部,会带大家离开新手村,爬取一个真正...

Day14 逻辑斯回归实作

https://github.com/PacktPublishing/Machine-Learni...

[Day2] Vite 出小蜜蜂~动画 Animation!

Day2 Animation 动画 动画在游戏中扮演非常重要的角色, 当绘制的角色在萤幕上动起来时,...

Day26 实现邮件寄送(1)

前几天教完各位自己创建middleware之後,不晓得大家是不是都理解清楚了呢! 而今天我们来跟各位...

[Day6] 自我必备沟通力:Content & Context

发挥影响力 随时必备的两个元素:Content & Context 自觉、找镜子、了解与掌握...