一、ReasonML 入门

在过去的十年里,我们构建用户界面的方式发生了无数的范式转变。Web 应用已经从服务器端框架转移到客户端框架,以便提供更好的用户体验。设备和浏览器已经变得足够强大,可以运行健壮的客户端应用,JavaScript 语言本身在过去几年里也有了许多改进。渐进式网络应用提供了类似本机的用户体验,而网络组装允许在网络平台上实现类似本机的性能。越来越多的应用正在为浏览器构建,导致需要维护更大的客户端代码库。

在此期间,几个框架、库、工具和通用最佳实践获得了人气,然后又失去了人气,导致许多开发人员产生了 JavaScript 疲劳。由于新技术对雇佣和留住工程人才以及生产率和可维护性的影响,公司在承诺采用新技术时变得越来越谨慎。如果你在错误的时间向你的团队引入了错误的技术(或者正确的技术),这可能是一个昂贵的错误。

对于许多公司和开发人员来说,React 已被证明是一个可靠的选择。2013 年,脸书在 2011 年内部使用该库后,将其开放源代码。他们要求我们重新思考最佳实践(https://www.youtube.com/watch?v=DgVS-zXgMTk&feature = youtu . be),此后它接管了前端开发(https://medium . freecodecamp . org/yes-react-is-接管前端开发-问题是为什么-40837af8ab76 )。将标记、行为和风格封装到可重用组件中已经成为生产力和可维护性的巨大胜利。DOM 的抽象使得组件成为简单的、声明性的功能,易于推理、组合和测试。

通过 React,脸书在对前端开发人员社区进行传统函数式编程范例的教育方面做了令人难以置信的工作,这使得推理和维护代码变得更加容易。现在,脸书相信时机已经成熟了。

这是来自 npmtrends.com 的两年图表,显示了一些顶级 JavaScript 库和框架的每周 npm 下载数量。ReactJS 似乎是一个明显的赢家,每周下载量超过 250 万次:

npmtrends.com

在本章中,我们将执行以下操作:

  • 讨论什么是理性,它试图解决什么问题
  • 理解脸书选择理性作为反应堆未来的一些原因
  • 在一个在线游戏场上试验 ReasonML 并检查它的编译(JavaScript)输出

什么是理性?

Reason is a layer of syntax & tooling on top of OCaml, a language Facebook uses actively. Jordan [Walke] started the concept of Reason before React, in fact. We’re taking it and using it as an actual frontend language (among other uses) because we think that after three and half years, the React experiment has succeeded and people are now ready for Reason... – Cheng Lou, January, 2017  (https://www.reactiflux.com/transcripts/cheng-lou/)

让我们进一步阐述这句话。原因语言不是一种新的语言;这是 OCaml 语言的一种新语法,是 JavaScript 开发人员所熟悉的。从现在开始,我们称之为 Reason,它与 OCaml 具有完全相同的 AST,因此 Reason 和 OCaml 仅在语法上有所不同。语义是一样的。通过学习理性,你也在学习 OCaml。事实上,有一个在 OCaml 和 Reason 语法之间转换的命令行工具,叫做refmt,它格式化 Reason/OCaml 代码,类似于 JavaScript 的 Beatle——事实上,Beatle 的灵感来源于refmt

OCaml 是一种强调表达性和安全性的通用编程语言。它最初于 1996 年发布,有一个先进的类型系统,可以帮助你抓住错误而不碍事。像 JavaScript 一样,OCaml 具有自动内存管理的垃圾收集和可以作为参数传递给其他函数的一流函数。

理性也是一个工具链,让那些来自 JavaScript 背景的人更容易上手。这个工具链允许我们利用 JavaScript 和 OCaml 生态系统。我们将在第 2 章设置开发环境中深入探讨。现在,我们将通过访问理智在https://reasonml.github.io/try的在线游乐场,直接在在线游乐场进行实验。

试着在这个你好世界的例子中输入在线游戏:

let message = "World";
Js.log("Hello " ++ message);

你会注意到两件事:

  • OCaml 语法是在编辑器的左下角自动生成的(未显示)
  • Reason/OCaml 代码直接在浏览器中编译为 JavaScript:
// Generated by BUCKLESCRIPT VERSION 3.2.0, PLEASE EDIT WITH CARE
'use strict';

var message = "World";

console.log("Hello World");

exports.message = message;
/* Not a pure module */

您可能想知道 Reason/OCaml 代码是如何在浏览器中编译的。Reason 的合作项目 BuckleScript 将 OCaml AST 编译为 JavaScript。因为原因和 OCaml 都被转换成同一个 OCaml AST,所以 BuckleScript 同时支持原因和 OCaml。此外,由于 BuckleScript 本身是用 OCaml 编写的,所以它可以编译成 JavaScript 并直接在浏览器中运行。

检查编译后的 JavaScript 会发现它的可读性有多强。仔细观察,您会注意到编译后的输出也进行了优化:在console.log语句中,"Hello World"字符串被直接内联,而不是使用message变量。

BuckleScript, using features of the OCaml type-system and compiler implementation is able to provide many optimizations during offline compilation, allowing the runtime code to be extremely fast. – BuckleScript docs  (https://bucklescript.github.io/bucklescript/Manual.html#_why_bucklescript)

值得注意的是,BuckleScript 还支持字符串插值(https://BuckleScript . github . io/docs/en/common-data-types . html #插值):

/* The message variable is interpolated */
{j|Hello $message|j}

为什么原因?

是什么让理性如此引人注目?理性能做什么类型脚本或流不能做的事情?仅仅是关于拥有一个静态类型检查器吗?这些是我第一次接触理性时遇到的一些问题。

支持不变性和纯度

原因不仅仅是拥有一个静态类型系统。同样重要的是,理性在默认情况下是不可变的。不变性是函数式编程中的一个重要概念。在实践中,使用不可变的数据结构(不能改变的数据结构)会产生比它们的可变对应物更安全、更容易推理和更易维护的代码。这将是贯穿本书的一个反复出现的主题。

纯度是函数式编程中的另一个重要概念。如果一个函数的输出仅仅由它的输入决定,而没有可观察到的副作用,那么这个函数就是纯函数。换句话说,纯函数除了返回值什么也不做。以下是纯函数的示例:

let add = (a, b) => a + b;

这是一个不纯函数的例子:

let add = (a, b) => {
  Js.log("side-effect");
  a + b;
};

这种情况下的副作用是写入浏览器的控制台。这就是为什么在我们前面的Hello World示例中,BuckleScript 在编译输出的末尾包含了/* Not a pure module */注释。

变异一个全局变量也是一个副作用。考虑以下 JavaScript:

var globalObject = {total: 0};
const addAndMutate = (a, b) => globalObject.total = a + b;
addAndMutate(40, 2);
/* globalObject now is mutated */

全局对象发生了变异,现在其total属性为42。我们现在必须意识到这个globalObject无论何时使用都会发生变异的所有领域。忘记这个对象是全局的和可变的会导致难以调试的问题。这个问题的一个惯用解决方案是将globalObject移动到不再是全局的模块中。这样,只有该模块可以访问它。然而,我们仍然需要知道这个模块中可以更新对象的所有区域。

如果globalObject反而是不可变的,那就没有办法变异了。因此,我们不需要知道所有可能变异的区域globalObject,因为这些区域都不存在。我们将看到,有了理性,通过创建原始数据的更新副本,以这种方式构建真正的应用是相当简单和自然的。请考虑以下几点:

let foo = 42;
let foo = foo + 1;
Js.log(foo);
/* 43 */

语法感觉很自然。正如我们将在本书后面看到的,不变性——通过返回更新的副本而不是就地应用破坏性的更改来进行更改——非常符合 React/Redux 的做事方式。

原来的foo没有变异;它被遮住了。一旦被遮蔽,旧的foo绑定将不可用。绑定可以在本地范围和全局范围中隐藏:

let foo = 42;

{
  let foo = 43;
  Js.log(foo); /* 43 */
};

Js.log(foo); /* 42 */

let foo = 43;
Js.log(foo); /* 43 */

试图变异foo会导致编译错误:

let foo = 42;
foo = 43;
/* compilation error */

我们可以看到不变性和纯度是相关的话题。拥有一种支持不变性的语言可以让你以一种没有副作用的纯方式编程。然而,如果有时候纯度会导致代码变得比使用副作用更复杂、更难推理,那该怎么办?当你了解到 Reason(在本书的剩余部分中可以与 OCaml 互换)是一种实用的语言,让我们在需要的时候产生副作用时,你可能会松一口气。

The key thing when using a language like [Reason] is not to avoid side-effects, because avoiding side-effects is equivalent to avoiding doing anything useful. It turns out, in reality, programs don't just compute things, they do things. They send messages and they write files and they do all sorts of stuff. The doing of things is automatically involving side-effects. The thing that a language which supports purity gives you, is it gives you the ability to, by and large, segment out the part that is side-effecting to clear and controlled areas of your code, and that makes it much easier to reason about. – Yaron Minsky (https://www.youtube.com/watch?v=-J8YyfrSwTk&feature=youtu.be&t=47m29s)

同样重要的是要知道不变性不是以性能为代价的。在引擎盖下,有适当的优化来保持理性的不可变数据结构快速。

模块系统

Reason 有一个复杂的模块系统,允许模块化开发和代码组织。所有模块在 Reason 中都是全局可用的,需要时可以使用模块接口隐藏实现细节。我们将在第 5 章有效 ML 中探讨这个概念。

类型系统

Reason 的类型系统是健全的,这意味着一旦编译,就不会有运行时类型错误。语言中没有null,也没有与null相关的 bug。在 JavaScript 中,当某物是number类型时,它也可以是null。Reason 对也可以是null的事物使用一种特殊的类型,并通过拒绝编译来迫使开发人员适当地处理这些情况。

到目前为止,我们已经编写了一些基本的原因代码,甚至没有讨论类型。原因自动推断类型。正如我们将在本书中学习的那样,类型系统是一个提供保证而不妨碍我们的工具,当使用得当时,它可以让我们把过去一直记在脑子里的东西卸载给编译器。

Reason 对不可变编程、声音类型系统和复杂模块系统的支持是 Reason 如此伟大的重要原因,在一种考虑到这些特性而构建的语言中,将所有这些特性结合在一起是有道理的。当脸书最初发布《反应》时,他们要求我们给它五分钟的时间(https://signalvnoise.com/posts/3124-give-it-five-minutes),希望同样的心态也能在这里得到回报。

跨平台

用理性构建 React 应用是一种可爱的体验,更重要的是,由于 OCaml 能够编译为原生语言,我们将能够使用这些相同的技能来构建编译为汇编语言、iOS/Android 等等的应用。事实上,杰瑞德·福赛思已经开发了一款名为 Gravitron(https://github.com/jaredly/gravitron)的游戏,它可以从一个 Reason 代码库中编译成 iOS、Android、web 和 macOS。也就是说,到本文撰写之时,前端的 JavaScript 故事已经更加完善了。

可维护性

理智可能需要一段时间来适应,但你可以把这段时间看作是对你未来产品的维护和信心的投资。尽管具有渐变类型系统的语言,如 TypeScript,可能更容易上手,但它们不能提供像 Reason 这样的健全类型系统所能提供的那种保证。理性的真正好处不能完全通过简单的例子来传达,只有当它们在推理、重构和维护代码方面为你节省时间和精力时,它才会真正发光。这么说吧;如果有人告诉我他们 99%确定我的床上没有蜘蛛,我还是要检查整个床,因为我不喜欢虫子!

只要你 100%在理性中,并且你的代码编译,类型系统保证不会有运行时类型错误。确实,当您与非原因代码(例如,JavaScript)交互时,您引入了运行时类型错误的可能性。Reason 的声音类型系统允许您相信应用的 Reason 部分不会导致运行时类型错误,因此允许您将额外的注意力集中在确保应用的这些区域是安全的。根据我的经验,用动态语言编程会有明显的危险。另一方面,理智感觉它总是支持着你。

互用性

也就是说,有时候——尤其是在第一次学习类型系统时——你可能不确定如何编译代码。通过 BuckleScript,Reason 允许您在需要时通过绑定或直接在 Reason ( .re)文件中下拉到原始 JavaScript。这让您可以自由地在 JavaScript 中解决问题,一旦准备好了,就可以将这段代码转换为类型安全的原因。

BuckleScript 还让我们以一种非常合理的方式绑定到惯用的 JavaScript。正如你将在第 4 章BuckleScript、Belt 和互操作性中了解到的,BuckleScript 是理性中极其强大的一部分。

这是 2030 年

用理性写作感觉就像用未来版本的 JavaScript 写作。一些推理语言特性,包括管道操作符(https://github.com/tc39/proposal-pipeline-operator)和模式匹配(https://github.com/tc39/proposal-pattern-matching)目前正被提交给 TC39 委员会,以添加到 JavaScript 语言中。有了理性,我们今天就可以利用这些特性,甚至更多。

社区

毫无疑问,理性社区是我参与过的最有帮助、最支持、最包容的社区之一。如果你有一个问题,或者被什么卡住了,原因不和谐频道是实时支持的地方。

Reason Discord channel:

https://discord.gg/reasonml

通常,当开始一项新技术时,与有经验的人交谈五分钟可以为你节省几个小时的沮丧。我个人在一天中的所有时间(和晚上)都在问问题,我非常感激和惊讶有人如此迅速地帮助我。花点时间加入 Discord 频道,自我介绍,提问,分享你对如何让 Reason 变得更好的反馈!

反应堆的未来

实际上,很少有真实世界的应用只使用 ReactJS。额外的技术,如 Babel、ESLint、Redux、Flow/TypeScript 和 invoke . js,通常被引入来帮助增加代码库的可维护性。Reason 用其核心语言特性取代了对这些附加技术的需求。

ReasonReact 是一个绑定到 ReactJS 的原因库,提供了一种更简单、更安全的方法来构建 ReactJS 组件。就像 ReactJS 只是 JavaScript 一样,React 只是 react。此外,增量采用很容易,因为它是由创建 ReactJS 的同一个人制作的。

原因 React 自带内置路由器、类似 Redux 的数据管理和 JSX。从 ReactJS 的背景中走出来,你会感觉很自在。

值得一提的是,React 已经被几家公司用于生产,包括世界上最大的代码库之一。脸书的 messenger.com 代码库已经有超过 50%被转换为 ReasonReact。

Every ReasonReact feature has been extensively tested on the messenger.com codebase. – Cheng Lou (https://reason.town/reason-philosophy)

因此,新版本的“原因”和“推理”都带有代码模块,可以自动完成代码库的大部分(如果不是全部)升级过程。新特性在向公众发布之前,在脸书进行了彻底的内部测试,这带来了愉快的开发者体验。

探索理性

问问你自己下面是一个陈述还是一个表达:

let foo = "bar";

在 JavaScript 中,它是一个语句,但在 Reason 中,它是一个表达式。表达式的另一个例子是4 + 3,也可以表示为4 + (2 + 1)

理智中的很多东西都是表达式,包括if-elseswitchforwhile等控制结构:

let message = if (true) {
  "Hello"
} else {
  "Goodbye"
};

我们在《理智》中也有术语。下面是表达前面代码的另一种方式:

let message = true ? "Hello" : "Goodbye";

甚至匿名块范围也是计算到最后一行表达式的表达式:

let message = {
  let part1 = "Hello";
  let part2 = "World";
  {j|$part1 $part2|j};
};
/* message evaluates to "Hello World" */
/* part1 and part2 are not accessible here */

A tuple是一个不可变的数据结构,可以保存不同类型的值,并且可以是任意长度:

let tuple = ("one", 2, "three");

让我们利用目前已知的知识,直接进入理性在线游乐场的FizzBuzz例子。FizzBuzz是一个流行的面试问题,用来确定候选人是否能够编码。面临的挑战是写一个打印从1100的数字的问题,而是打印三的倍数的Fizz,五的倍数的Buzz,以及三和五的倍数的FizzBuzz:

/* Based on https://rosettacode.org/wiki/FizzBuzz#OCaml */
let fizzbuzz = (i) =>
  switch (i mod 3, i mod 5) {
  | (0, 0) => "FizzBuzz"
  | (0, _) => "Fizz"
  | (_, 0) => "Buzz"
  | _ => string_of_int(i)
  };

for (i in 1 to 100) {
  Js.log(fizzbuzz(i))
};

这里,fizzbuzz是接受整数并返回字符串的函数。命令for循环将其输出记录到控制台。

在推理中,函数的最后一个表达式成为函数的返回值。switch表达式是唯一的fizzbuzz表达式,所以无论结果如何都将成为fizzbuzz的输出。像 JavaScript 一样,switch计算一个表达式,第一个匹配的案例执行它的分支。在这种情况下,switch计算元组表达式:(i mod 3, i mod 5)

给定i=1(i mod 3, i mod 5)成为(1, 1)。由于(1, 1)不是由(0, 0)(0, _)(_, 0)匹配的,所以按照这个顺序,最后一个_(即任意一个匹配,返回"1"。同样,当给出i=2时,fizzbuzz返回"2"。给出i=3时,返回"Fizz"

或者,我们可以使用if-else实现fizzbuzz:

let fizzbuzz = (i) =>
  if (i mod 3 == 0 && i mod 5 == 0) {
    "FizzBuzz"
  } else if (i mod 3 == 0) {
    "Fizz"
  } else if (i mod 5 == 0) {
    "Buzz"
  } else {
    string_of_int(i)
  };

但是,switch 版本可读性更强。正如我们将在本章后面看到的,switch 表达式,也称为模式匹配,比我们目前看到的要强大得多。

数据结构和类型

类型是一组值。更具体地说,42具有int类型,因为它是包含在整数集合中的值。浮点数是包含小数点的数字,即42.42.0。在推理中,整数和浮点数有单独的运算符:

/* + for ints */
40 + 2;

/* +. for floats */
40\. +. 2.;

-.-*.*/./也是如此。

Reason 对string类型使用双引号,对char类型使用单引号。

创造我们自己的类型

我们还可以创建自己的类型:

type person = (string, int);

/* or */

type name = string;
type age = int;
type person = (name, age);

下面是我们如何创建一个person类型的人:

let person = ("Zoe", 3);

我们还可以用类型来注释任何表达式:

let name = ("Zoe" : string);
let person = ((name, 3) : person);

模式匹配

我们可以对我们的人使用模式匹配:

switch (person) {
| ("Zoe", age) => {j|Zoe, $age years old|j}
| _ => "another person"
};

让我们为我们的人使用记录而不是元组。记录类似于 JavaScript 对象,只是它们轻得多,并且默认情况下是不可变的:

type person = {
  age: int,
  name: string
};

let person = {
  name: "Zoe",
  age: 3
};

我们也可以在记录上使用模式匹配:

switch (person) {
| {name: "Zoe", age} => {j|Zoe, $age years old|j}
| _ => "another person"
};

像 JavaScript 一样,{name: "Zoe", age: age}可以表示为{name: "Zoe", age}

我们可以使用价差(...)运算符从现有记录创建新记录:

let person = {...person, age: person.age + 1};

记录需要类型定义才能使用。否则,编译器会出现如下错误:

The record field name can't be found.

记录的形状必须与其类型相同。因此,我们不能在person记录中添加任意字段:

let person = {...person, favoriteFood: "broccoli"};

/*
  We've found a bug for you!

  This record expression is expected to have type person
  The field favoriteFood does not belong to type person
*/

元组和记录是产品类型的例子。在我们最近的例子中,我们的person类型需要一个int和一个age。几乎所有 JavaScript 的数据结构都是产品类型;一个例外是boolean型,要么是true要么是false

原因的变体类型,是求和类型的一个例子,允许我们表达这个或那个。我们可以将boolean类型定义为变体:

type bool =
  | True
  | False;

我们可以有任意多的构造函数:

type decision =
  | Yes
  | No
  | Maybe;

YesNoMaybe被称为构造函数,因为我们可以用它们来构造值。它们通常也被称为标签。因为这些标记可以构造值,所以变量既是类型又是数据结构:

let decision = Yes;

当然,我们可以在decision上进行模式匹配:

switch (decision) {
| Yes => "Let's go."
| No => "I'm staying here."
| Maybe => "Convince me."
};

如果我们忘记处理一个案例,编译器会警告我们:

switch (decision) {
| Yes => "Let's go."
| No => "I'm staying here."
};

/*
  Warning number 8

  You forgot to handle a possible value here, for example: 
  Maybe
*/

正如我们将在第 2 章设置开发环境中了解到的,编译器可以配置为将此警告转化为错误。让我们看看一种方法,通过利用这些穷尽性检查来帮助我们的代码更好地适应未来的重构。

以下面的例子为例,我们的任务是计算给定区域的音乐会场地座位的价格。落地座位是 55 美元,而所有其他座位是 45 美元:

type seat =
  | Floor
  | Mezzanine
  | Balcony;

let getSeatPrice = (seat) =>
  switch(seat) { 
  | Floor => 55
  | _ => 45
  };

如果以后,音乐会场地允许以 65 美元的价格出售乐池区域的座位,我们将首先在seat中添加另一个构造函数:

type seat =
  | Pit
  | Floor
  | Mezzanine
  | Balcony;

但是,由于使用了包罗万象的_用例,我们的编译器在这一更改后没有抱怨。如果这样做就更好了,因为这将在我们的重构过程中帮助我们。在更改类型定义后逐步浏览编译器消息是 Reason(以及一般的 ML 语言家族)如何使重构和扩展代码成为一个更安全、更愉快的过程。当然,这不限于变体类型。向我们的person类型添加另一个字段也会导致同样的遍历编译器消息的过程。

相反,我们应该为无限多的情况保留使用_(比如我们的fizzbuzz例子)。我们可以重构getSeatPrice来使用显式用例:

let getSeatPrice = (seat) =>
  switch(seat) { 
  | Floor => 55
  | Mezzanine | Balcony => 45
  };

在这里,我们欢迎编译器很好地通知我们未处理的情况,然后添加它:

let getSeatPrice = (seat) =>
  switch(seat) {
  | Pit => 65
  | Floor => 55
  | Mezzanine | Balcony => 45
  };

现在让我们想象一下,每个座位,甚至同一个区域的座位(也就是标签相同的座位)都可以有不同的价格。原因变量也可以保存数据:

type seat =
  | Pit(int)
  | Floor(int)
  | Mezzanine(int)
  | Balcony(int);

let seat = Floor(57);

我们可以通过模式匹配来访问这些数据:

let getSeatPrice = (seat) =>
  switch (seat) {
  | Pit(price)
  | Floor(price)
  | Mezzanine(price)
  | Balcony(price) => price
  };

变体不仅仅局限于一条数据。让我们假设我们希望我们的seat类型存储它的价格以及它是否仍然可用。如果没有,它应该存储持票人的信息:

type person = {
  age: int,
  name: string,
};

type seat =
  | Pit(int, option(person))
  | Floor(int, option(person))
  | Mezzanine(int, option(person))
  | Balcony(int, option(person));

在解释option类型是什么之前,我们先来看看它的实现:

type option('a)
  | None
  | Some('a);

前面代码中的'a被称为类型变量。类型变量总是以'开头。此类型定义使用类型变量,因此它可以适用于任何类型。如果没有,我们需要创建一个仅适用于person类型的personOption类型:

type personOption(person)
  | None
  | Some(person);

如果我们也想要另一种类型的选择呢?我们声明一个多态类型,而不是一遍又一遍地重复这个类型声明。多态类型是包含类型变量的类型。在我们的示例中,'a(发音为 alpha)类型变量将与person交换。由于这种类型定义非常常见,它包含在理性的标准库中,所以没有必要在代码中声明option类型。

回到我们的seat例子,我们将其价格存储为int,其持有者存储为option(person)。如果没有持有者,它仍然可用。我们可以有一个isAvailable函数,它会取一个seat并返回一个bool:

let isAvailable = (seat) =>
  switch (seat) {
  | Pit(_, None)
  | Floor(_, None)
  | Mezzanine(_, None)
  | Balcony(_, None) => true
  | _ => false
  };

让我们后退一步,看看getSeatPriceisAvailable的实现。遗憾的是,当这两个功能与座椅的价格或可用性无关时,它们需要意识到不同的构造器。再看看我们的seat类型,我们看到(int, option(person))对于每个构造函数都是重复的。此外,也没有什么好的方法可以避免使用isAvailable中的_案例。这些都表明另一种类型定义可能更好地满足我们的需求。让我们从seat类型中移除参数,并将其重命名为section。我们将声明一个新的记录类型,称为seat,其字段为sectionpriceperson:

type person = {
  age: int,
  name: string,
};

type section =
 | Pit
 | Floor
 | Mezzanine
 | Balcony;

type seat = {
  section, /* same as section: section, */
  price: int,
  person: option(person)
};

let getSeatPrice = seat => seat.price;

let isAvailable = seat =>
  switch (seat.person) {
  | None => true
  | Some(_person) => false
  };

现在我们的getSeatPriceisAvailable功能的信噪比更高了,不需要在section类型改变时改变。

顺便提一下,_用于给变量加前缀,以防止编译器警告我们该变量未被使用。

使无效状态变得不可能

假设我们想在seat中添加一个字段来保存购买座位的日期:

type seat = {
  section,
  price: int,
  person: option(person),
  dateSold: option(string)
};

现在,我们已经在代码中引入了无效状态的可能性。这里有一个这样的状态的例子:

let seat = {
  section: Pit,
  price: 42,
  person: None,
  dateSold: Some("2018-07-16")
};

理论上,dateSold字段应该只有当person字段持有持票人时才持有日期。这张票有售出日期,但没有主人。我们可以通过我们想象中的实现来验证这种状态永远不会发生,但是仍然有可能我们错过了一些东西,或者一些小的重构引入了一个被忽略的 bug。

既然我们现在拥有了推理类型系统的能力,让我们把这项工作交给编译器。我们将使用类型系统在代码中实施不变量。如果我们的代码违反了这些规则,它就不会编译。

这种无效状态可能存在的一个证据是在我们的记录字段中使用了option类型。在这些情况下,可能有一种方法可以改为使用变量,这样每个构造函数只保存相关数据。在我们的案例中,我们的售出日期和持票人数据只应在座位售出时存在:

type person = {
  age: int,
  name: string,
};

type date = string;

type section =
  | Pit
  | Floor
  | Mezzanine
  | Balcony;

type status =
  | Available
  | Sold(date, person);

type seat = {
  section,
  price: int,
  status
};

let getSeatPrice = (seat) => seat.price;

let isAvailable = (seat) =>
  switch (seat.status) {
  | Available => true
  | Sold(_) => false
  };

看看我们新的status型。Available构造器不保存数据,Sold保存售出日期和持票人。

有了这个seat类型,就没有办法表示之前没有持票人的卖出日期的无效状态了。这也是一个好迹象,我们的seat型不再包括option型。

摘要

在这一章中,我们感受到了什么是理性,理性试图解决什么问题。我们看到了理性的类型推理如何消除了与静态类型语言相关的许多负担。我们了解到,类型系统是一种工具,可以用来为代码库提供强大的保证,从而提供出色的开发人员体验。虽然理性可能需要一些时间来适应,但对中型到大型代码库的投资是非常值得的。

在下一章中,我们将在建立开发环境时了解 Reason 的工具链。在第 3 章创建推理推理组件中,我们将开始构建一个应用,我们将在本书的剩余部分中使用它。到这本书的最后,你会很容易在理性中构建真实世界的反应应用。