剑客
关注科技互联网

Elm提供的语言级响应性

在不断发展的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

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址