• home > webfront > ECMAS > react >

    React代数效应学习笔记

    Author:zhoulujun Date:

    代数效应是一项研究中的编程语言特性,用于将副作用从函数调用中分离,实现代码在自调用函数中自上而下执行的操作。简而言之,代数效应是一种异常(exception)机制,可让throw编码功能继续其操作。

    参考文章:

    通俗易懂的代数效应 https://overreacted.io/zh-hans/algebraic-effects-for-the-rest-of-us/

    代数效应与React https://zhuanlan.zhihu.com/p/169805499

    以类hooks编程践行代数效应 https://www.tangshuang.net/7899.html

    代数效应与React https://juejin.cn/post/6857673031016251406

    javascript - FP中的代数效应是什么意思? https://www.coder.work/article/224326



    React hooks在框架编程上具有明显特征,在推广functional组件的进程中,javascript是天然具有函数式编程优势的语言,因此,react团队越来越倾向并重视hooks的应用。hooks编程之所以拥有比较大的魅力,除了它抹平class组件和functional组件在生命周期上的差异之外,更重要的是,它让react开发者践行代数效应。

    Hooks属于React版本的一个新特性,其目的是全面拥抱函数,之前函数式组仅仅是作为展示组件,它的内部并不关心数据是怎么加载和变动的,只能通过props的方式接收和进行callback操作,也并不具备calss组件相似的生命周期,所以函数式组件的使用场景很少。

    自16.8版本起,React就开始全面提供了hooks的稳定版,使开发者在不使用calss组件的情况下,拥有自己的生命周期和状态更新机制,从此函数式组件的地位直线提升。

    它的发起人是Sebastian Markbåge(如图),除了抹平class组件和function组件在生命周期的差异外,按他本人的说法,其目的是为了践行`代数效用(Algebraic Effects)。

    如果你只是用 React, 其实完全没有必要去了解这些概念 —— 但如果你像我一样对此感到好奇,就请继续读下去吧。

    React核心团队成员Sebastian Markbåge(React Hooks的发明者)曾说:我们在React中做的就是践行代数效应(Algebraic Effects)。

    那么,代数效应是什么呢?

    什么是代数效应?

    代数效应是一项研究中的编程语言特性,用于将副作用从函数调用中分离,实现代码在自调用函数中自上而下执行的操作

    这意味着不像 if,functions,甚至比较新的 async / await,你也许还不能在生产中真正的使用它。只有少数专为研究这些特性而创造的语言支持它们的使用。但我们可以选择去了解它的存在,就像1999年有人尝试理解async一样!

    在理解什么是“代数效应”之前,请先阅读一下这篇文章。为了理解“代数”,我们举一个例子:

    理解“代数”

    已知:

    y + 2x = 9 (1)

    2y + x = 15 (2)

    求:

    x + y = ?

    --------------------------

    由 (1) 可得:

    y = 9 - 2x (3)

    将 (3) 代入 (2) 可得:

    2(9 - 2x) + x = 15 

    => 18 - 3x = 15 

    => x = 1 (4)

    将 (4) 代入 (3) 可得:

    y = 7

    将求得的x, y代入x + y可得

    x + y = 8

    这就是代数,本质上,代数是研究函数(数的关系)的科学,它的精髓在于“代入”这个动作,它的主要方法是解方程

    想象一下,上面的题,如何用程序来解决呢?不,我的意思是,你所写的程序,如何解出 3x² + y²  = ? 甚至更多的算式?这和我们往常的编程思路恰恰相反,我们以往的编程思路,是通过不同点测算出函数,再根据函数获得另一个点,这是机器学习的基本思路。但是,我们的编程,尚未有以代数为基本的思路。

    代数要解决的问题

    常见的编程思路,已知A、B,求C点

    编程思路

    代数要解决的问题,已知A在1、2函数上,求函数3的方程

    在第2个图中,已知的是1、2两个函数都通过A点,但A点具体值我们并不知道,要求出的,是任何可能穿过A的其他函数的方程。

    JS原生代数效应

    在本节开头引用的那篇文章里提到了,总结而言,代数效应,是让编程(代码)可以自上而下书写,但让程序的执行可以在不同函数间跳跃的编程效果。代数效应对我们编程有什么启示呢?

    我们可以尝试虚构一段代码去实现剥离副作用的操作,试想当我们还在写回调地狱时,有人给你看了一眼async/generator,会不会很cool?

    const featchList = (param1,param2) => {
        const num1 = fetchNum(param1);  
        const num2 = fetchNum(param2);
        return num1 + num2
    }

    接下来我们需要去了解什么是副作用? 

    如上,在调用featchList函数时,我们希望通过fetNum函数计算num1和num2的值,最后返回两数相加的结果。

    接下来我们需要去了解什么是副作用? 如上,在调用featchList函数时,我们希望通过fetNum函数计算num1和num2的值,最后返回两数相加的结果。

    const fetchNum = params => {
       let resp;
       new Promise( (resolve,reject) => {
          setTimeOut( ()=> {
            const resp = params;
            resolve(resp);
          })
       })
       return resp
    }

    如上,在fetchNum函数中我们模拟了一段异步的请求操作,最终两数相加的结果就是NaN(null+null=NaN),这 就是副作,也是我么为什么要剥离副作用的原因。

    那javaScript现有语法是如何处理副作用操作的? 首先在不改变函数主体的情况下,我们考虑到的是使用await/yield,我们接下来去了解一下async/yield的实现

    const featchList = async (param1,param2) => {
        const num1 = await getNum(param1);  
        const num2 =  await getNum(param2);
        return num1+num2
    }

    或者

    funciton *featchList() {
        const num1 = yield getNum(param1);  
        const num2 = yield getNum(param2);
        return num1+num2
    }

    以及for await...of语法,都是具有代数效应的编程方式,因为:

    如上,当程序执行碰到await/yield时,程序会从原有的函数执行流中跳到另外一个执行流中完成副作用(异步处理),并将副作用结果返回给当前执行流,从而达到程序在不同函数间跳跃的目的

    简单说,js中的代数效应表达方式,让我们通过await和yield语法,让程序从原有的函数执行流中,跳到另外一个执行流中完成副作用,并将副作用结果返回给当前执行流,再用这个结果进行剩下的计算。所以说,上面说的“函数间跳跃”的主要目的,是将函数式和副作用进行分离,保持函数式编程的同时,又支持副作用操作

    但是,async/await和generator函数具有传染性,它们要求所有外部编程在语法上必须采用不可替代的表示式,从而让代数效应的实现不具备普适性和通用性。

    当调用函数featchList使用了async修饰符时,它的返回值变成了promise,类似的,如果featchList使用了generator修饰符,它的返回值就变成了Generator 。这意味着,调用featchList的函数外层也需要包裹一层async/*修饰符,这样看来await/yield并不具备普适性和通用性。

    那我们可不可以在不改变函数主体的情况下,处理异步请求呢?

    不可以,但接下来我们会模拟一段代码(try...handle)进行副作用剥离的操作,就像async未被纳入ECMA规范时,有人尝试理解async一样。

    模拟代数效应的实现

    在js领域,不止一人提出了新的语法以支持这种编程方法。比较典型的编程方法如下:

    try {

      const v1 = do1()

      const v2 = raise '1'

      const v3 = do3(v2)

      const v4 = raise '3'

      return v4

    handle (e) {

      if (e === '1') {

        resume 'v2'

      }

      else if (e === '3') {

        resume 'v4'

      }

    }


    关键字都用红色标注出来了(虚构语法)。它和 try...catch 一样,通过 throw 抛出异常,通过 catch 捕获异常一样,在这段代码中,通过 raise 抛出代数陷阱,通过 handle 捕获陷阱,在捕获块中应对(处理)陷阱,通过 resume 跳出陷阱,将处理结果带出陷阱作为值继续执行 try 块中的剩余代码。

    它和 try...catch的区别仅仅在于try...catch一旦throw,后续代码就不会再执行。而try...handle不仅可以持续执行至代码块结束,而且由于resume的使用可以是随意的,所以在handle中可以写异步操作,从而在无await/yeild的情况下,让异步操作变得更加像同步操作。

    除了在形式上的新颖有趣,更重要的是实用性。

    在以前,我们要为一个函数提供某种修改的能力,我们也会尝试类似的方法,例如:

    function calcZ() {
      const x = calcX()
      const y = calcY()
      const z = x + y
      return z
    }

    我们有这样一个函数,我们将这个函数提供给其他人,他就可以用它来计算z的值。对于使用它的人而言,需要提供calcX和calcY函数,这样,他就可以自定义x和y的计算逻辑。这在简单全局场景是可以的,但在模块化的今天,则不可行,除非要求写全局的calcX和calcY函数。


    但通过try...handle则让这个自定义x和y的计算逻辑变得简单:

    function calcZ() {
      const x = raise 'x'
      const y = raise 'y'
      const z = x + y
      return z
    }

    对于使用者而言:

    try {
      const z = calcZ()
    }
    handle (e) {
      switch (e) {
        case 'x': {
          resume 10
          break
        }
        case 'y': {
          resume 15
          break
        }
      }
    }

    甚至,在handle中进行异步操作:

    try {
      const z = calcZ()
    }
    handle (e) {
      switch (e) {
        case 'x': {      setTimeout(() => {
            resume 10      }, 1000)
          break
        }
        case 'y': {
          resume 15
          break
        }
      }
    }

    这样,对于原始函数calcZ而言,它完全是同步的,但我们却可以异步的获取x的值,在异步求x值后,代入原来的函数中继续执行后续计算。

    至此,什么是代数效应?简而言之,代数效应是一种异常(exception)机制,可让throw编码功能继续其操作

    尝试将代数效应视为某种try/catch机制,其中catch处理程序不仅仅“处理异常”,而且能够为引发异常的函数提供一些输入。然后,将catch处理程序的输入用于throwing函数中,该函数将继续运行,好像没有异常一样。

    Hooks中的代数效应

    既然hooks的发明者Sebastian Markbåge说hooks在践行代数效应,那么我们是否需要换一种思维,去理解hooks的运行原理。

    function App() {
      const [num, updateNum] = useState(0)
      return (
        <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
      )
    }

    我们将上面这段代码进行翻译:

    function App() {
      const num = raise 'state_num'
      return <button>{num}</button>
    }

    我们去掉干扰信息,得到上面这段简单代码,而hooks帮我们在react库内部完成了如下工作:

    function render() {
      let state_num // 建立了一个变量用于缓存
    
      try {
        return App()
      }
      handle (e) {
        if (e === 'state_num') {
          return typeof state_num === 'undefined' ? 0 : state_num
        }
      }
    }

    这就是hooks在践行的代数效应。将上面的代码进行扩展,我们可以非常方便的再写出updateNum的实现方式。


    当然,除了hooks,Suspense还有Reconciler,都是对代数效应的践行,它们本质上就如我前文所说,在正常的程序流程中,允许我们停下来,去做另外一件事,做完之后,我们可以再从被打断的地方继续往下执行,而另外的那件事,可以是同步的,也可以是异步的,理论上,它的执行过程与我们当前的流程无关,我们仅关心(或根本不关心)它的结果。

    类hooks编程

    React hooks在实践代数效应,我们能否在其他环境下(非react相关)也仿造hooks的思想,践行代数效应?问题的关键点在于,js并没有try...handle语法!

    我希望创建一种类hooks的编程。例如:

    function calc() {
      const x = get('x')
      const y = get('y')
      return x + y
    }

    如何实现呢?我们可以再提供一个接口,让开发者规定get('x')要做什么事情:

    define('x', function(set) {
      setTimeout(() => {
        set(100)
      }, 1000)
    })

    看,当执行get('x')时,define的第二个参数会被执行,这个参数,就是try...handle中的handle部分。但是,calc是同步的,而获取x的过程是异步的,怎么办呢?再计算一次!

    setup(function() {
      const z = calc()
    })

    当set(100)被执行时,setup内的函数再次执行,这样,z就可以被再计算一次,而此时get('x')的结果为100。



    转载本站文章《React代数效应学习笔记》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2021_0717_8647.html