Elm 提供的语言级响应性

阅读数:1314 2016 年 11 月 9 日

话题:JavaScript函数式编程语言 & 开发

在不断发展的 JavaScript 编程领域,响应性编程技术正变得愈加流行。这一系列文章试图向大家介绍该方法目前的进展,介绍各种可用技术,以及该领域产生的变化。从 Elm 等新语言到 Angular 2 对 RxJS 的支持,无论从事什么工作的开发者均有相关新技术可供使用。

InfoQ 的这篇文章已包含在“响应性 JavaScript”系列文章中。你可以订阅RSS并在内容更新后获得通知。

响应性编程可以让 JavaScript 程序员的生活变得更美好... 但如果能使用围绕响应性量身定制的语言来编程呢?

Elm 编程语言的目标就是如此。虽然其他实现响应性的方法会对 JavaScript 进行渐进式的改进,而 Elm 会从最基础的地方开始重新构建这一切。Elm 的诞生不是为了回答诸如“JavaScript 如何能变得更好”之类的问题,而是为了回答“构建 Web 用户界面时,最棒的整体开发者体验是怎样的”这样的问题。

事实证明如果以此为目标进行设计,最终将获得一种与 JavaScript 截然不同的语言!为了实现这一目标,Elm 采用了一些 JavaScript 完全不具备的特征:

  • 以响应性作为对交互做出响应的唯一系统
  • 通过一种一致的方式管理不同特效(Effect)
  • 通过更好用的编译器提前排除大量 Bug

借助这些特征,最终获得了 Elm,一种可编译为 JavaScript,但摒弃了 JavaScript 弱点的语言。我们经常听说有人在生产环境中运行的 Elm 代码从未遇到一个运行时异常,甚至最令人担心的“未定义(Undefined)”也从未出现过。

Elm 到底是如何实现这种截然不同的体验的?这一切都源自它的架构。

Elm 的架构

你可能已经听说过,Elm 架构的一些灵感来自 Redux 和其他响应式 JavaScript 库。该架构会将应用程序拆分为三个简单的部件:

  • 模型(Model)
  • 更新(Update)
  • 视图(View)

模型是一种代表整个应用程序状态的常量值,更新是一种获取当前模型和消息(消息是一种描述模型所期望变化的常量值)并返回修订后模型的函数,视图是一种接受当前模型并返回所需 DOM 结构表征的函数。

Elm 的运行时通过连接这三种部件即可组成响应式应用程序。当用户点击一个按钮后,只允许出现一个结果:向更新函数发送一条消息。这样就可以在相关内容之间实现良好的区分:所有应用程序逻辑均通过更新函数实现,所有渲染逻辑都通过视图函数实现,所有应用程序状态均存储在模型中。

Elm 架构可以通过一种简单的方式实现模块化。如果需要对一些渲染逻辑进行分隔,可以编写另一个接受主视图函数调用的视图函数。如果模型变得大到离谱,可以建立一个更小的模型并将其嵌入主模型中。如果需要一个自行管理自己状态的独立组件,可以为其创建模型、视图和更新,并将其以子“对象”的方式委派给父模型、视图和更新。

这种从根本上进行简化的架构需要一定的适应过程,但随着代码基规模的扩大,这种方法可以确保一切井然有序。无论调用多少帮助(Helper)函数,所有应用程序状态均能嵌套在主模型中,所有应用程序逻辑均能嵌套在主更新函数中,所有渲染逻辑均能嵌套在主视图函数中。

对全局事件的响应也变得更简单。编写一个可以查询当前模型的订阅函数,确定应用程序要订阅的事件(从键盘按键到 WebSocket 接收到数据,一切事件均可订阅),随后将这些事件转换为消息并发送给更新函数。

在模型、视图和更新中使用这种集中化的逻辑,意味着 Elm 的响应性只产生很少量的清理工作。我们可以创建、重配置,或移除事件侦听器和可观察对象(Observable),这意味着需要追踪更多内容。但 Elm 架构中无需追踪这些,只需要通过可选的订阅函数将消息发送至为 onClick 等 DOM 事件始终使用的同一个更新函数即可。

相比其他可编译为 JavaScript 的语言,例如 CoffeeScript、Dart,以及 ClojureScript,Elm 最大的不同不仅在于增加的功能,还在于舍弃的功能。这种模型 - 视图 - 更新架构并不是 Elm 建议的做法,而是编写应用程序的唯一做法!这意味着 Elm 包仓库中的每个库均围绕这一想法构建,再也不需要决定备选的渲染策略该如何选择。开发者只需要通过一种能得到极为完善支持的方法完成自己的工作。

托管的作用

在响应式 JavaScript 库文档中,最常见的一个警告通常是“别在这里产生副作用。”Elm 并不需要这样的警告,因为 Elm 只支持托管的作用(Managed effect),不会产生副作用,而托管的作用不会产生类似副作用导致的问题。

在托管作用系统中不需要立刻执行作用,而是需要描述希望用数据做些什么。Flux 存储所发起调用产生的副作用可能会立刻发起 HTTP 请求,而在 Elm 的更新函数中,除了返回常规的模型更新,还可以返回所要进行的 HTTP 请求对应的描述。Elm 的运行时会负责将这些有关 HTTP 请求的描述转换为实际的 HTTP 请求。

此处最重要的差异在于,在托管作用系统中,API 可以可靠地强制指定哪些函数可以返回作用的描述。例如更新函数可以返回新模型以及曾完成的任何作用的描述... 但视图函数只能返回自己需要的 DOM 的描述。非预期的副作用可在 API 层面上彻底排除!

更棒的是,托管作用很好地解决了响应式 JavaScript 领域一些常见问题的一致性。一些 API 使用 Promise,另一些使用回调(Callback)事件,同步方面还有其他副作用... 但在 Elm 中这些都只是任务(Task)。

任务类似于回调,其本身的实例化(Instantiating)是无害的,我们可以对数百个描述 HTTP 请求的任务进行实例化,但此时不会产生任何网络活动。一旦将任务从一个函数传递至另一个函数并最终交给 Elm 运行时,随后才会执行这些任务。任务与 Promise 类似,可以链在一起,并包含了类似的一类错误处理机制,如果链中任何任务失败,其余任务将不被执行,整个链会处于这个失败值的状态下。

围绕 Promise 有一个常见痛点:会产生吞咽(Swallow)异常。Elm 的任务不会遇到这种问题,因为 Elm 中完全没有任何异常!如果某个作用可以失败,唯一的失败方法是借助任务内建的失败处理机制。Elm 没有类似 Try/catch 的机制,没有类似 Throw 的机制,只有任务。

Elm 可以实现这一特性是因为所有作用都是以任务的形式实现的,而这也意味着所有失败的作用必然会使用任务的错误处理系统。对比而言,JavaScript 的错误处理会包含各种例外:被拒绝的 Promise,以及有时候可能传递给回调的错误参数。Elm 有关作用的一致性意味着不存在类似冲突失败机制等问题,例如异常和被拒绝的 Promise。

以编译器为后盾

“只要能编译,通常就能正常工作”,这种说法对 Elm 程序员已经很熟悉了。

产生这种说法并不是因为 Elm 的编译器具有奇迹般的除错能力,而是因为该编译器可以强制确保整个架构尽量简单。语言级响应性,以及使用托管的作用代替副作用,这些特性可以消除大量可能导致出错的因素,其余“漏网之鱼”通常可以通过编译器提早发现并解决。

例如拼写错误的字段名就是一种常见错误。假设打算输入 phoneNumber 但无意中输入了 phoenNumber,此时也许会看到类似这样的错误信息:

最终用户绝对不会受到这个 Bug 的影响,因为该错误只会出现在编译时。更棒的是,开发者完全不需要通过调用堆栈的方式回溯并调试,最后才收到一条有关“phoneNumber 未定义”之类的错误信息。Elm 的编译器不仅可以提前发现这种问题,甚至可以直接标出有问题的代码行数。

这种体验最棒的地方在于能够为代码重构工作起到的作用。对整个代码基进行大量大规模的变更不可避免会导致编译器错误(毕竟程序员总会犯各种错误),但在解决编译器错误时... 正如那句话所说,“通常就能正常工作”。这一点真正让人感觉耳目一新!所有“未定义”都是函数,都是可以正常工作不会崩溃的代码。

通过这种方式开发者还可以免费获得全面的测试能力。Elm 的编译器可以自动验证所有组件是否以合理的方式连接在一起,使得开发者无需自行开发其他能实现与 Elm 编译器同等程度的预防性测试方法。开发者编写的测试数量更少,但代码变得更可靠。

更棒的是,Elm 的包管理器可以感知这些保证,并使用这些保证强制实施自动化的语义版本控制。如果任何人试图发布包含破坏性 API 变更的包,包管理器会拒绝发布,除非变更包含到主版本号的 Bump。如果想了解任何包的任何两个版本之间 API 的差异,可以运行类似 elm-package diff NoRedInk/elm-rails 2.0.0 3.0.0 这样的命令,借此查看 2.0.0 和 3.0.0 版包之间的变化。

Elm 的JavaScript 互操作系统按照设计可以维持这些保证。此时并不需要与 JavaScript 共享(可能非常易于崩溃的)代码,Elm 应用程序可以用类似于与服务器或 Web 工作进程通信的方式与 JavaScript 代码通信:往返发送数据。唯一的差别在于:并不需要通过网络或 Web 工作进程传输数据,Elm 会与其他语言传输数据。

很多团队会谈到“Elm 领域”和“JavaScript 领域”,这两种代码基应用了不同的规则。在 Elm 领域开发者可以很放松并确信 Elm 编译器可以防止代码崩溃。在 JavaScript 领域就不那么确信了,开发者在内心深处会深信不疑地觉得代码中某处存在着测试未发现的忘掉的 Null 检查。

通过保持这两个领域相互独立,仅通过数据进行通信,Elm 在提供最佳响应式编程体验的同时使得开发者能够继续从庞大的 JavaScript 库生态系统获益。

总结

在目前可供 Web 开发者选择的各种可用选项中,Elm 对响应性提供了最全面的支持:从语言本身实现了响应性。除了在设计上可以帮助开发者从最基础的响应性设计中获得最大化收益的编译器外,Elm 还为开发者提供了:

  • 运行时零异常成为常态—“只要能编译,通常就能正常工作。”
  • 通过语言提供一类支持,简单的应用程序架构。
  • 在链式作用和错误处理过程中使用一致、令人愉悦的 API。
  • 无需担心有问题的副作用影响响应性。
  • 实用的编译时错误信息帮助减少测试数量的同时实现更可靠的代码。

Elm 依然是一种相对较新的语言,但NoRedInkPreziFuturiceGizraCircuitHub等很多公司已经开始使用这种语言开发生产应用程序。

如果想要进一步了解 Elm,可以参考下列资源:

  1. Elm 简介 – Elm 创造者 Evan Czaplicki 提供的指南
  2. 使用 Elm 构建可实时验证的注册表单 – 针对 JavaScript 程序员提供的教程
  3. 重新思索所有实践:用 Elm 构建应用程序 - ReactConf 访谈
  4. 让后端团队嫉妒:Elm 在生产中的应用 - Strange Loop 访谈
  5. Elm in Action,Manning Publications 即将出版的图书

关于本文作者

Richard Feldman长期以来一直担任 Web 程序员的职位,是 React 和 Elm 的早期采用者。他担任了 Frontend Masters Elm Workshop 讲师的角色,并写了一本名为《Elm in Action》的书,此书即将由 Manning Publications 出版。他就职于 NoRedInk,大部分时间通过编写 Elm 代码帮助学生了解语法和编程。

在不断发展的 JavaScript 编程领域,响应性编程技术正变得愈加流行。这一系列文章试图向大家介绍该方法目前的进展,介绍各种可用技术,以及该领域产生的变化。从 Elm 等新语言到 Angular 2 对 RxJS 的支持,无论从事什么工作的开发者均有相关新技术可供使用。

InfoQ 的这篇文章已包含在“响应性 JavaScript”系列文章中。你可以订阅RSS并在内容更新后获得通知。

作者:Richard Feldman阅读英文原文:Language-Level Reactivity with Elm