编程的本质是状态机
编程的本质是状态机
越来越觉得大多数编程工作本质上就是在写状态机。特别是 Web 开发这块,前后端本质上都在写状态机,而前端尤甚——视图层不就是个不能更明显的状态机。
状态机与视图
在各个前端框架中,React 尤其体现了“状态机”这一概念——它都有一个 hook 叫useState了,“state”这个关键词都明明白白地写了出来,这可不就是状态机。在 React 函数式组件中,我们用setState切换到下一个状态,然后根据当前状态渲染视图,这再明显不过。
而一切状态管理方案,本质上就是让状态之间的切换更清晰且可控,比如经典的 Flux 方案——Redux 和 Vuex 都是该方案的实现。只允许定义一连串 action 来修改 state,即通过 dispatch 函数调用dispatch({ type: ..., payload: ... })来修改状态,其中的{ type: ..., payload: ... }就是 action,用 type 来标识是哪一个 action,用 payload 传参。Flux 方案使用单向数据流,数据只能从上往下传,不能从下往上,并且每一次修改状态都要通过 action,这就保证了状态切换的清晰性,也(在项目比较复杂的时候)提高了程序的可维护性。
很多“响应式方案”,如 Vue 的 ref/reactive 和 MobX,状态切换并不明显,但其背后的原理是一致的。这不过是在 ref 对象(或其他框架中随便什么叫法,叫什么无所谓)上挂了个监听器,当监听到修改时去切换状态,其实是一致的。其实我认为响应式的方案会比较好,它虽然模糊了状态切换的清晰性,但使用更符合直觉的方式编程。即使遇到复杂的状态,也有其他的状态管理库做单向数据流,比如 Vue 自己出的 Vuex 就是为了补足这点。
状态机与协程
再扩展一些,一切“协程”或类似的如 Goroutine 之类的变形,甚至 JS/Python 里面带的那个基于事件循环的勉强可以被称作“协程”的 async/await,都是状态机。无论它们的实现如何,究竟是传统的为每个协程保留一个栈然后用状态机管理,还是无栈协程保留一个 Global 栈,还是 Goroutine 那种轻量级的变形,还是 JS/Python 这种基于生成器+事件循环的方式,也都是在做状态之间的切换。
这里尤其要讲一下生成器。据我所知被广泛使用的生成器可能只有 JS 和 Python 的生成器,当然自从 JS 有了 async/await 以来用生成器的人也很少了。如果有人看过 JS 生成器编译到 ES5 的代码的话,可以发现转译器通常将生成器转译为一个状态机,会在里面写一个大大的 Switch,根据不同的 state 执行不同代码片段。
在 JS 的 async/await 出现之前,就出现了大量诸如 co 的库使用生成器来模拟协程,其实相比现在只是把 await 改成了 yield,然后需要在每个 async 函数外面包个 co 并且声明为生成器函数而已,甚至有些地方还比现在更强大。
协程的目的仅仅是在合适的时候主动交出执行权,并且合适的时候拿回它,比如一个文件 IO 协程在等待 IO 前将执行权交出,然后在 IO 结束时恢复,在这期间可以由其他等待的协程获取执行权,不浪费等待的时间。而生成器的 yield 做的事情和协程刚好一样,只需要在外面包装个环境协调多个生成器函数即可。因此生成器完全是可以模拟协程的。
而生成器本身又可以轻松转译为状态机,而写个状态机对于绝大多数语言来说都是非常轻松的事情。这也意味着协程本质上是不依赖于语言自身提供的什么高级特性的。C 语言都不是不可以实现协程,只是缺少关键字支持用起来会很别扭而已。协程本身也是可以作为标准库的一部分而实现的,如 Kotlin 就是这么做的。
可见协程并非如线程、进程这样的底层概念,它只是在此之上的一层抽象,一层状态机抽象。协程不一定需要操作系统甚至编程语言的原生支持。
我逐渐认识到状态机应该是每个人,或者至少计算机相关专业的学生应该掌握的东西。而这些年来国内各高校都逐渐减少了对计算理论相关知识的教学,很多相关课程成为选修甚至直接消失,只有编译原理中还会教授一些相关知识,告诉你可以用 FA 做解析器,这是很遗憾的。