模块

基础知识

模块就像迷你文件!它们可以包含类型定义、let 绑定、嵌套模块等。

创建模块

使用 module 关键字来创建模块。模块的名字必须以大写字母开头。任何你可以放在 .res 文件的东西都可以放置在模块定义的 {} 块中。

ReScriptJS Output
module School = {
  type profession = Teacher | Director

  let person1 = Teacher
  let getProfession = (person) =>
    switch person {
    | Teacher => "A teacher"
    | Director => "A director"
    }
}

模块的内容(包括类型!)可以像记录一样使用 . 访问,下面的代码演示了模块的命名空间功能。

ReScriptJS Output
let anotherPerson: School.profession = School.Teacher
Js.log(School.getProfession(anotherPerson)) /* "A teacher" */

模块也可以嵌套。

ReScriptJS Output
module MyModule = {
  module NestedModule = {
    let message = "hello"
  }
}

let message = MyModule.NestedModule.message

打开(open)模块

不断引用模块中的值/类型会很啰嗦。我们可以 open 一个模块,引用它的内容,而不必总是在它们前面加上模块的名称。除了这样写:

ReScriptJS Output
let p = School.getProfession(School.person1)

我们还可以这样:

ReScriptJS Output
open School
let p = getProfession(person1)

School 模块的内容在作用域内可见(不是复制到文件中,仅仅是可见而已!)。这样就可以正确地找到 professiongetProfessionperson1

尽量少用 open,它很方便,但是很难知道值是从哪来的。你通常应该在局部作用域中使用 open

ReScriptJS Output
let p = {
  open School
  getProfession(person1)
}
/* School's content isn't visible here anymore */

使用 open! 来忽略覆盖警告

有些情况下,由于现有的标识符(绑定、类型)被重定义了,open 会引发警告。使用 open! 来明确地告诉编译器这是所希望的行为。

RES
let map = (arr, value) => { value } // opening Js.Array2 would shadow our previously defined `map` // `open!` will explicitly turn off the automatic warning open! Js.Array2 let arr = map([1,2,3], (a) => { a + 1})

注意:和 open 一样,如果没有必要,就不要过度使用 open! 语句。使用(子)模块来解决覆盖问题。

解构模块

从 9.0.2 版本开始

作为 open 模块的替代方法,你也可以将模块的函数和值解构为单独的 let 绑定(类似于我们在 JavaScript 中解构对象)。

ReScriptJS Output
module User = {
  let user1 = "Anna"
  let user2 = "Franz"
}

// Destructure by name
let {user1, user2} = module(User)

// Destructure with different alias
let {user1: anna, user2: franz} = module(User)

注意:你不能使用模块解构来提取类型 —— 使用类型别名来代替(type user = User.myUserType)。

扩展模块

在模块中使用 include ,可以静态地将一个模块的内容“扩散”到一个新的模块中,因此 include 经常发挥“继承(inheritance)”和“混合(mixin)”的作用。

注意:这相当于编译器级别的复制粘贴。我们非常不鼓励使用 include将它作为最后的手段。

ReScriptJS Output
module BaseComponent = {
  let defaultGreeting = "Hello"
  let getAudience = (~excited) => excited ? "world!" : "world"
}

module ActualComponent = {
  /* the content is copied over */
  include BaseComponent
  /* overrides BaseComponent.defaultGreeting */
  let defaultGreeting = "Hey"
  let render = () => defaultGreeting ++ " " ++ getAudience(~excited=true)
}

注意openinclude 是非常不同的!前者将模块的内容带入当前的作用域中,这样你不必每次都用模块名字做前缀来引用一个值。后者是静态地复制模块的定义,然后也会执行一个 open 操作。

每一个 .res 文件都是一个模块

每个 ReScript 文件本身被编译成与文件同名(首字母大写)的模块。文件 React.res 隐式地成为了 React 模块,其他源文件可以看到这个模块。

注意:按照惯例,ReScript 文件名应该首字母大写,以使它们的名字与模块名称匹配。不大写的文件名不是无效的,它们会隐式地转换为大写的模块名。例如 file.res 将会被编译为模块 File。为了简化和减少这种脱节感,惯例是将文件名首字母大写。

签名

模块的类型被称为“签名”,可以显式地写出。如果将模块对应于 .res(实现)文件,那么模块的签名就像一个 .resi(接口)文件。

创建签名

使用 module type 关键字来创建签名。签名名字必须首字母大写。任何你可以放在 .resi 文件的东西都可以放置在签名定义的 {} 块中。

ReScriptJS Output
/* Picking up previous section's example */
module type EstablishmentType = {
  type profession
  let getProfession: profession => string
}

签名定义了一个列表,包含模块必须满足的要求,以使该模块符合签名。要求如下:

  • let x: int 表示需要一个名为 xlet 绑定,类型为 int

  • type t = someType 要求类型字段 t 等于 someType

  • type t 表示需要一个类型字段 t,但没有对 t 的实际具体类型提出任何要求。我们会在签名的其他条目中使用 t 来描述关系。例如 let makePair: t => (t, t),我们无法推出 tint。这给了我们强大的,强制的抽象能力。

为了说明各种类型的条目,请考虑上面的 EstablishmentType 签名,它要求一个模块:

  • 声明一个名为 profession 的类型。

  • 必须包含一个接受 profession 类型值并返回字符串的函数。

注意

EstablishmentType 类型的模块可以包含比在签名中声明的更多的字段,就像之前章节中的模块 School 一样(如果我们选择为 School 赋予 EstablishmentType 类型。否则 School 会暴露所有字段)。这使得 person1 字段成为了实现细节!外部无法访问它,因为它不存在于签名中;签名约束了其他人可以访问的内容。

类型 EstablishmentType.profession抽象的:它没有一个具体的类型;它表示“我不关心实际的类型是什么,但是它被用作 getProfession 的输入”。这对于在同一接口下容纳多个模块是很有用的:

ReScriptJS Output
module Company: EstablishmentType = {
  type profession = CEO | Designer | Engineer | ...

  let getProfession = (person) => ...
  let person1 = ...
  let person2 = ...
}

隐藏底层类型也很有用,因为这是一个别人无法依赖的实现细节。如果你问 Company.profession 的类型是什么,Company 不会暴露这个变体,而只会告诉你它是 Company.profession

扩展模块签名

像模块一样,模块签名也可以使用 include 来扩展其他模块签名。这也是非常不推荐的

ReScriptJS Output
module type BaseComponent = {
  let defaultGreeting: string
  let getAudience: (~excited: bool) => string
}

module type ActualComponent = {
  /* the BaseComponent signature is copied over */
  include BaseComponent
  let render: unit => string
}

注意BaseComponent 是一个模块类型,而不是实际的模块本身!

如果你没有定义的模块类型,你可以使用 include (module type of ActualModuleName) 从实际模块中提取。例如,我们可以扩展标准库的 List 模块,它并没有定义模块类型。

ReScriptJS Output
module type MyList = {
  include (module type of List)
  let myListFun: list<'a> => list<'a>
}

每个 .resi 文件都是签名

React.res 文件隐式定义了 React 模块类似,React.resi 隐式定义了 React 签名。如果没有提供 React.resiReact.res 的签名默认为暴露模块的所有字段。因为它们不包含实现文件,.resi 文件也用于记录对应模块的公共 API。

ReScriptJS Output
/* file React.res (implementation. Compiles to module React) */
type state = int
let render = (str) => str
/* file React.resi (interface. Compiles to the signature of React.res) */ type state = int let render: string => string

模块函数(函子)

模块可以作为参数传递给函数!它相当于将文件作为一等公民传递。然而,模块与其他常见概念处于不同的语言“层级”,所以我们不能将它们传递给常规函数。相反,我们将它们传递给名为“函子”的特殊函数。

用于定义和使用函子的语法和常规函数非常类似,它们的主要区别是:

  • 函子使用 module 关键字而不是 let

  • 函子将模块作为参数并返回一个模块。

  • 函子需要对参数进行标注。

  • 函子的名字必须以大写字母开头(就像模块/签名一样)。

下面是一个 MakeSet 函子实例,它接受一个 Comparable 类型的模块,并返回一个可以包含这种可比(comparable)项的新集合。

ReScriptJS Output
module type Comparable = {
  type t
  let equal: (t, t) => bool
}

module MakeSet = (Item: Comparable) => {
  // let's use a list as our naive backing data structure
  type backingType = list<Item.t>
  let empty = list{}
  let add = (currentSet: backingType, newItem: Item.t): backingType =>
    // if item exists
    if List.exists(x => Item.equal(x, newItem), currentSet) {
      currentSet // return the same (immutable) set (a list really)
    } else {
      list{
        newItem,
        ...currentSet // prepend to the set and return it
      }
    }
}

函子可以使用函数应用的语法。在本例中,我们创建了一个集合,其元素项是成对的整数。

ReScriptJS Output
module IntPair = {
  type t = (int, int)
  let equal = ((x1: int, y1: int), (x2, y2)) => x1 == x2 && y1 == y2
  let create = (x, y) => (x, y)
}

/* IntPair abides by the Comparable signature required by MakeSet */
module SetOfIntPairs = MakeSet(IntPair)

函子类型

与模块类型一样,函子类型也起到约束和隐藏函子的作用。函子类型的语法与函数类型的语法一致,但是使用首字母大写的类型表示函子接受的参数和返回值的签名。在前面的例子中,我们暴露了集合的内部类型;通过给 MakeSet 一个函子签名,我们可以隐藏底层的数据结构!

ReScriptJS Output
module type Comparable = ...

module type MakeSetType = (Item: Comparable) => {
  type backingType
  let empty: backingType
  let add: (backingType, Item.t) => backingType
}

module MakeSet: MakeSetType = (Item: Comparable) => {
  ...
}

奇异(Exotic)的模块文件名

从 8.3 开始

可以在文件名中使用非常规字符(这有时是特定 JS 框架所需要的)。这里有一些例子:

  • src/Button.ios.res

  • pages/[id].res

请注意,其他 ReScript 模块将不能访问具有奇异文件名的模块。

技巧和诀窍

模块和函子与语言的其他部分(函数、let 绑定、数据结构等)处于不同的“层级”。例如,你不能轻易地将它们传入元组或记录中。如果需要的话,请谨慎地使用它们!很多时候只需要记录或函数就够了。