模块
基础知识
模块就像迷你文件!它们可以包含类型定义、let
绑定、嵌套模块等。
创建模块
使用 module
关键字来创建模块。模块的名字必须以大写字母开头。任何你可以放在 .res
文件的东西都可以放置在模块定义的 {}
块中。
模块的内容(包括类型!)可以像记录一样使用 .
访问,下面的代码演示了模块的命名空间功能。
模块也可以嵌套。
打开(open)
模块
不断引用模块中的值/类型会很啰嗦。我们可以 open
一个模块,引用它的内容,而不必总是在它们前面加上模块的名称。除了这样写:
我们还可以这样:
School
模块的内容在作用域内可见(不是复制到文件中,仅仅是可见而已!)。这样就可以正确地找到 profession
、getProfession
和 person1
。
尽量少用 open
,它很方便,但是很难知道值是从哪来的。你通常应该在局部作用域中使用 open
:
使用 open!
来忽略覆盖警告
有些情况下,由于现有的标识符(绑定、类型)被重定义了,open
会引发警告。使用 open!
来明确地告诉编译器这是所希望的行为。
RESlet 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 中解构对象)。
注意:你不能使用模块解构来提取类型 —— 使用类型别名来代替(type user = User.myUserType
)。
扩展模块
在模块中使用 include
,可以静态地将一个模块的内容“扩散”到一个新的模块中,因此 include
经常发挥“继承(inheritance)”和“混合(mixin)”的作用。
注意:这相当于编译器级别的复制粘贴。我们非常不鼓励使用 include
。将它作为最后的手段。
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)
}
注意:open
和 include
是非常不同的!前者将模块的内容带入当前的作用域中,这样你不必每次都用模块名字做前缀来引用一个值。后者是静态地复制模块的定义,然后也会执行一个 open
操作。
每一个 .res
文件都是一个模块
每个 ReScript 文件本身被编译成与文件同名(首字母大写)的模块。文件 React.res
隐式地成为了 React
模块,其他源文件可以看到这个模块。
注意:按照惯例,ReScript 文件名应该首字母大写,以使它们的名字与模块名称匹配。不大写的文件名不是无效的,它们会隐式地转换为大写的模块名。例如 file.res
将会被编译为模块 File
。为了简化和减少这种脱节感,惯例是将文件名首字母大写。
签名
模块的类型被称为“签名”,可以显式地写出。如果将模块对应于 .res
(实现)文件,那么模块的签名就像一个 .resi
(接口)文件。
创建签名
使用 module type
关键字来创建签名。签名名字必须首字母大写。任何你可以放在 .resi
文件的东西都可以放置在签名定义的 {}
块中。
签名定义了一个列表,包含模块必须满足的要求,以使该模块符合签名。要求如下:
let x: int
表示需要一个名为x
的let
绑定,类型为int
。type t = someType
要求类型字段t
等于someType
。type t
表示需要一个类型字段t
,但没有对t
的实际具体类型提出任何要求。我们会在签名的其他条目中使用t
来描述关系。例如let makePair: t => (t, t)
,我们无法推出t
是int
。这给了我们强大的,强制的抽象能力。
为了说明各种类型的条目,请考虑上面的 EstablishmentType
签名,它要求一个模块:
声明一个名为
profession
的类型。必须包含一个接受
profession
类型值并返回字符串的函数。
注意:
EstablishmentType
类型的模块可以包含比在签名中声明的更多的字段,就像之前章节中的模块 School
一样(如果我们选择为 School
赋予 EstablishmentType
类型。否则 School
会暴露所有字段)。这使得 person1
字段成为了实现细节!外部无法访问它,因为它不存在于签名中;签名约束了其他人可以访问的内容。
类型 EstablishmentType.profession
是抽象的:它没有一个具体的类型;它表示“我不关心实际的类型是什么,但是它被用作 getProfession
的输入”。这对于在同一接口下容纳多个模块是很有用的:
隐藏底层类型也很有用,因为这是一个别人无法依赖的实现细节。如果你问 Company.profession
的类型是什么,Company
不会暴露这个变体,而只会告诉你它是 Company.profession
。
扩展模块签名
像模块一样,模块签名也可以使用 include
来扩展其他模块签名。这也是非常不推荐的:
注意:BaseComponent
是一个模块类型,而不是实际的模块本身!
如果你没有定义的模块类型,你可以使用 include (module type of ActualModuleName)
从实际模块中提取。例如,我们可以扩展标准库的 List
模块,它并没有定义模块类型。
每个 .resi
文件都是签名
与 React.res
文件隐式定义了 React
模块类似,React.resi
隐式定义了 React
签名。如果没有提供 React.resi
,React.res
的签名默认为暴露模块的所有字段。因为它们不包含实现文件,.resi
文件也用于记录对应模块的公共 API。
/* file React.resi (interface. Compiles to the signature of React.res) */
type state = int
let render: string => string
模块函数(函子)
模块可以作为参数传递给函数!它相当于将文件作为一等公民传递。然而,模块与其他常见概念处于不同的语言“层级”,所以我们不能将它们传递给常规函数。相反,我们将它们传递给名为“函子”的特殊函数。
用于定义和使用函子的语法和常规函数非常类似,它们的主要区别是:
函子使用
module
关键字而不是let
。函子将模块作为参数并返回一个模块。
函子需要对参数进行标注。
函子的名字必须以大写字母开头(就像模块/签名一样)。
下面是一个 MakeSet
函子实例,它接受一个 Comparable
类型的模块,并返回一个可以包含这种可比(comparable)项的新集合。
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
}
}
}
函子可以使用函数应用的语法。在本例中,我们创建了一个集合,其元素项是成对的整数。
函子类型
与模块类型一样,函子类型也起到约束和隐藏函子的作用。函子类型的语法与函数类型的语法一致,但是使用首字母大写的类型表示函子接受的参数和返回值的签名。在前面的例子中,我们暴露了集合的内部类型;通过给 MakeSet
一个函子签名,我们可以隐藏底层的数据结构!
奇异(Exotic)的模块文件名
从 8.3 开始
可以在文件名中使用非常规字符(这有时是特定 JS 框架所需要的)。这里有一些例子:
src/Button.ios.res
pages/[id].res
请注意,其他 ReScript 模块将不能访问具有奇异文件名的模块。
技巧和诀窍
模块和函子与语言的其他部分(函数、let 绑定、数据结构等)处于不同的“层级”。例如,你不能轻易地将它们传入元组或记录中。如果需要的话,请谨慎地使用它们!很多时候只需要记录或函数就够了。