【干货分享】来聊一聊Algebraic Effect

前言

React16.8推出了Hooks, 并介绍了为什么要推出Hooks

都说React Hooks其实是Algebraic Effect, 今天我们就来一探究竟。

TOC

今天的文章会主要包括如下几个话题,感兴趣的朋友可以往下看看呀~

  1. 什么是CPS
  2. 控制转移
  3. JS中Throw的行为
  4. 介绍Algebraic Effect
  5. Algebraic Effect解决的问题
  6. React Hooks与Algebraic Effect的关系

1. 什么是 CPS

在讲Algebraic Effect之前我们需要介绍Continuation-passing style (CPS)。

In functional programming, continuation-passing style is a style of programming in which control is passed explicitly in the form of a continuation.
A function written in continuation-passing style takes an extra argument: an explicit “continuation”, i.e. a function of one argument. When the CPS function has computed its result value, it “returns” it by calling the continuation function with this value as the argument. That means that when invoking a CPS function, the calling function is required to supply a procedure to be invoked with the subroutine’s “return” value. Expressing code in this form makes a number of things explicit which are implicit in direct style. These include: procedure returns, which become apparent as calls to a continuation; intermediate values, which are all given names; order of argument evaluation, which is made explicit; and tail calls, which simply call a procedure with the same continuation, unmodified, that was passed to the caller.

摘自 Wikipedia?

✍️ 翻译成中文并用 JS 表达:

  • CPS 是一种编程风格, 通过回调函数来返回结果和控制流程
  • 在 CPS 风格中, 每一个函数都会显式的接收一个回调函数(continuation),
  • 通过回调函数来传递当前函数计算结果并控制下一个函数的调用。

用 JavaScript 代码演示 (1+2)×2=6( 1 + 2 ) \times 2 = 6
image.png

如果我们将上面的过程进行分解,将操作符用函数来表达,则会变成这样
image.png

通常我们习惯通过函数的返回值将结果给调用端, 那如果按照CPS的风格改造下会长什么样呢?
image.png

每一个函数的都接受一个回调函数 next, 且计算结果都通过 next 延续给下一个函数

直呼好家伙, CPS原来就是被我们嗤之以鼻回调地狱

2. 控制转移

上面以JavaScript解释了什么是CPS, 那这有什么用呢?

通过CPS可以提升对程序流程控制

正常版本 中流程的控制是随代码逐行执行的。

在执行完let temp = add(1, 2)后自然而然的执行 let result = multi(temp, 2)

假设有个产品经理忽然提了一个需求 “add这个过程需要 1s 后才返回结果”

对于正常版本我们需要对add后的整个流程进行一个控制
image.png

在CPS风格版本的例子中, 由于add方法的next拥有对后续流程的控制能力, 因此我们只需要调整 add 函数中next的调用时机即可以满足需求
image.png

我是不会写回调地狱的, 这辈子都不会!

的确, CPS虽然很强大, 但却不适合人类书写

因此 ES6的Promise试图通过链式调用的方式以提升代码阅读性
image.png
之前产品的需求也可以很简单的实现
image.png

人们对于使用同步书写是十分偏执的

Promise版本看起来已经很棒了,但是能不能更符合人性化一点呢.
在2013年TJ推出了co, 利用Generator Function更加接近了这个目标
image.png

现在ES7 async/await已经在JavaScript落地, 我们可以比较流畅的使用同步的方式来书写

Promise, Co, async/await 其实都属CPS变换

还记得最开始介绍CPS有提到 “当前函数拥有对后续流程的控制”吗?
Promise.resolve(), generaotr.next() 都是 CPS 例子里的 next
image.png

3. JS 中 Throw 的行为

在JavaScript中, 常见控制流程的方式有 if else , while break 等方式.
如果我们将对流程的控制理解为”可以决定后续代码知否执行”,那么可以认为throw也是一种控制流程的手段, 它可以跳过后续流程

throw 除了也拥有对流程的控制外还有一个特殊的能力 — 穿透调用栈

跨调用栈不是目的, 目的是为了可以获取当前的Continuation

throw操作符会在调用出处中断,并将throw操作符后的内容随调用栈向上冒泡, 直到被try catch捕获

一般代码规范告诉我们throw必须是一个Error对象, 但实际可以throw任意JavaScript值,哪怕是一个函数

举一个例子:
image.png

4.介绍Algebraic Effect

到这里我们应该可以达成2点共识

  • CPS风格的代码里是通过next控制后续流程, 并通过next回调返回调用结果
  • JavaScript throw 操作也是一种流程控制手段, 且throw具有调用栈穿透的效果

那么如果把这2点合并成一种行为会是怎么样呢?

这个行为就和我们今天想要介绍的Algebraic Effect很接近了

我们天马行空地造3个特殊关键字performhandle还有resume来尝试表达对应的效果

  1. 我们假定关键字 perform 它可以像之前提到的 CPS 变换, 类似await关键字
  2. perform修饰的方法可以被 像throw一样穿透调用栈被try handle捕获
  3. resume拥有对后续流程的控制权, 并且可以通过resume返回计算结果 (类似Promiseresolve)

我们把CPS next回调称之为Continuation,希望通过throw可以跨调的特点获取当前 Continuation,跨调用栈不是本意, 目的可以获取当前上下文中当前的Continuation,以实现所谓的 callcc

之所以要造新关键字来表示,是因为JavaScript中的throw后并不能恢复,要实现callcc需要对调用栈有比较强的控制, 本文重点还是以介绍Algebriac Effect为主,因此就不过多讲解callcc的实现了

image.png
上述代码其实就是在表达 Algebraic Effect, 我们根据上述代码来进行概括性的说明

  1. 通过perform + resume实现对后续流程的控制, 或者说实现了可以上下文重入的效果
  2. 通过try handle可以实现跨调用栈捕获当前continuation

5. Algebraic Effect解决的问题

那为什么要这么做呢?

按照Algebraic Effect概念的字面拆解:

  • Algebraic:代数式,可以理解成初中数学的换元法
  • Effect: 最容易联想的即副作用,非纯的部分

我个人理解是分离了 “主逻辑做什么”和 “副作用实现”,是一种用对 “纯”的追求,也像函数式编程依赖注入的一种实现

6.React Hooks与 Algebraic Effect的关系

Algebraic Effect有2个特点

  1. 可以跨调用栈获取当前Continuation
  2. 可以替换当前Continuation和Effect的实现

那么为什么说React Hooks是Algebraic Effect呢?或者说React Hooks是否满足上述2中点特呢?

我们来看一个React Hooks例子:
image.png

1.可以跨调用栈获取当前Continuation

useHook实现了穿透调用栈效果,不论hooks如何抽象嵌套,只要保证是同步调用hooks,都能正确作用于对应组件.

我们可以将每一个Function Component看作是React里的Continuation, 对Continuation 的调度是交给React Fiber控制的.

2. 可以替换当前Continuation内Effect的实现

Algebraic Effect模式通过定义Effect抽象表明做什么, 通过实现Handler来决定要如何做. 从而来隔离主逻辑中的”非纯”部分

useHook(callback)

而之所以说React也是一种Algebraic Effect是因为React内置hook是对React组件里副作用的抽象, 注册的callback则是该组件副作用的实现.

从以上角度React Hook和Algebraic Effect是有不少相同之处的.

留几个问题给读者:

  1. Hooks为什么要是静态的, (不能在 if else 里写)
  2. Concurrent mode: 为什么要 throw 一个 Promise ?

参考内容

  1. Farrow 使用 Algebraic Effect(利用 nodeJS async_hooks) + TypeScript 打造的类型安全的函数式 HTTP Framework

Algebraic in JS

Koka

Effekt

OCaml – effects tutorial

Dan Abramov Blog

React Fiber

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享