原文:原文:http://2ality.com/2017/12/modules-reasonml.html
翻译:ppp
系列文章目录详见: “什么是ReasonML?”
在本文中,我们将探讨ReasonML中模块的工作原理。
本文中所有例子的代码都可以在GitHub找到: reasonml-demo-modules。下载并安装:
cd reasonml-demo-modules/
npm install
你只需要这样做就可以了 - 不用全局安装。如果你不仅仅是想运行一下代码,而是希望对ReasonML有更多了解,请参阅“ReasonML入门”。
这是你第一个ReasonML程序的目录:
reasonml-demo-modules/
src/
HelloWorld.re
在ReasonML中,每一个类型为.re的文件都是一个模块。模块名称以大写字母开头,并且按照驼峰命名法。文件名即是它的模块名,所以它们遵循相同的规则。 一个程序就是你从命令行运行的一个模块。
HelloWorld.re 看起来如下:
/* HelloWorld.re */
let () = {
print_string("Hello world!");
print_newline()
};
这段代码可能看起来有点奇怪,所以让我来解释一下:我们正在执行花括号内的两行代码,并将它们的运行结果分配给模式()。也就是说,这段代码并不会创建新变量,但可以确保的是运行结果就是()。()的类型,unit是类似于C语言中的void。
请注意,我们并没有定义函数,我们只是立即执行了print_string()和print_newline()。
为了编译这段代码,你有两个选择(查看package.json了解更多可以运行的脚本):
一次性编译: npm run build
监控所有文件,当文件内容发生改变时编译: npm run watch
因此,我们的下一步是(在单独的terminal窗口中运行或在最后一步执行):
cd reasonml-demo-modules/
npm run watch
在HelloWorld.re相同目录下,会有一个新文件HelloWorld.bs.js。 你可以按如下所示运行该文件。
cd reasonml-demo-modules/
node src/HelloWorld.bs.js
作为我们方法的替代方案(这是OCaml常见的一种惯例),我们也可以简单地将这两行放入全局范围:
/* HelloWorld.re */
print_string("Hello world!");
print_newline();
或者我们可以定义一个称之为main()的函数:
/* HelloWorld.re */
let main = () => {
print_string("Hello world!");
print_newline()
};
main();
让我们继续,MathTools.re模块被Main.re模块所引用:
reasonml-demo-modules/
src/
Main.re
MathTools.re
模块MathTools如下:
/* MathTools.re */
let times = (x, y) => x * y;
let square = (x) => times(x, x);
模块Main如下:
/* Main.re */
let () = {
print_string("Result: ");
print_int(MathTools.square(3));
print_newline()
};
正如你所看到的,在ReasonML中,你可以通过直接通过文件名来使用该模块。在项目中的任何位置都可以。
模块也可以嵌套定义。所以这也行:
/* Main.re */
module MathTools = {
let times = (x, y) => x * y;
let square = (x) => times(x, x);
};
let () = {
print_string("Result: ");
print_int(MathTools.square(3));
print_newline()
};
在外部,你可以通过这样Main.MathTools来访问MathTools 让我们再进一步:
/* Main.re */
module Math = {
module Tools = {
let times = (x, y) => x * y;
let square = (x) => times(x, x);
};
};
let () = {
print_string("Result: ");
print_int(Math.Tools.square(3));
print_newline()
};
默认情况下,该模块的所有值和类型都会被导出。如果你想隐藏其中一些值或类型的话,你必须要使用接口。此外,接口支持抽象类型(其内部实现也是隐藏的)。
你可以通过接口控制具体要导出的内容。对于一个由文件Foo.re定义的模块,其接口应放在文件中Foo.rei。例如:
/* MathTools.rei */
let times: (int, int) => int;
let square: (int) => int;
例如,如果你把times从接口文件中去掉,那它就不会被导出。
模块的接口也称为其模块的签名。
如果存在接口文件,则必须把文档注释也放在里面。否则,你要把它们放到.re文件中。
幸运的是,我们不需要手动编写接口,我们可以直接从模块中生成接口(如 in the BuckleScript documentation所述)。例如MathTools.rei,我是通过以下方式生成的:
bsc -bs-re-out lib/bs/src/MathTools-ReasonmlDemoModules.cmi
我们假设,MathTools并不在它自己的文件中,而是以子模块的形式定义:
module MathTools = {
let times = (x, y) => x * y;
let square = (x) => times(x, x);
};
我们如何为这个模块定义一个接口呢?我们有两个选择。 首先,我们可以通过module type定义并命名一个接口
module type MathToolsInterface = {
let times: (int, int) => int;
let square: (int) => int;
};
该接口就成为模块MathTools的一个类型:
module MathTools: MathToolsInterface = {
···
};
此外,我们也可以以内联的方式定义:
module MathTools: {
let times: (int, int) => int;
let square: (int) => int;
} = {
···
};
你可以使用接口来隐藏类型的具体实现。让我们以模块Log.re为例,它的功能是打日志。通过字符串实现日志功能,并把字符串直接打印出来:
/* Log.re */
let make = () => "";
let logStr = (str: string, log: string) => log ++ str ++ "\n";
let print = (log: string) => print_string(log);
从上面的代码,我们其实并不清除make()和logStr()实际上返回的是日志。 Log的用法如下。注意管道操作符(|>)在这种情况下很方便:
/* LogMain.re */
let () = Log.make()
|> Log.logStr("Hello")
|> Log.logStr("everyone")
|> Log.print;
/* Output:
Hello
everyone
*/
改进Log的第一步是引入一种日志类型。我们参考OCaml,也用t表示模块支持的主要类型。例如:Bytes.t
/* Log.re */
type t = string; /* A */
let make = (): t => "";
let logStr = (str: string, log: t): t => log ++ str ++ "\n";
let print = (log: t) => print_string(log);
在A行中,我们将t定义为字符串的别名。别名很方便,你可以快速上手稍后在添加更多功能。但是,别名也要求我们申明make()和logStr()的返回类型(否则返回值类型会是string)。 完整的接口文件如下所示。
/* Log.rei */
type t = string; /* A */
let make: (unit) => t;
let logStr: (string, t) => t;
let print: (t) => unit;
我们可以用下面的代码替换A行,并且t变成抽象的 - 它的具体实现将被隐藏。这意味着我们可以在未来轻松改变我们的想法,例如通过数组来实现它。
type t;
但我们不需要修改LogMain.re,新模块任然适用于它,这点很方便。
有多种方法可以实现模块的导入。
我们已经看到,当你调用模块的接口时就自动导入了该模块。例如,在下面的代码我们从Log模块导入make,logStr和print:
let () = Log.make()
|> Log.logStr("Hello")
|> Log.logStr("everyone")
|> Log.print;
如果你通过open显示的导入了Log,下面的代码就不需要再方法名前面加上Log.了:
open Log;
let () = make()
|> logStr("Hello")
|> logStr("everyone")
|> print;
为避免名称冲突,不常这么用。大多数模块,如List,都是通过模块名调用的:List.length(),List.map(),等。 全局也可用于选择标准模块的不同实现。例如,模块Foo可能有一个子模块List。然后全局open Foo将覆盖标准List模块。
通过局部openLog,我们可以最大限度地减少命名冲突的风险,同时还能获得全局open的便利。我们通过在Log.后面加括号来实现。例如:
let () = Log.(
make()
|> logStr("Hello")
|> logStr("everyone")
|> print
);
在ReasonML中,很方便的一点是操作符也是一个函数。这使我们可以暂时覆盖内置的操作符。例如,我们可能不喜欢使用带点的运算符来进行浮点运算:
let dist = (x, y) =>
sqrt((x *. x) +. (y *. y));
然后我们可以通过FloatOps 模块来覆盖int 的操作符:
module FloatOps = {
let (+) = (+.);
let (*) = (*.);
};
let dist = (x, y) =>
FloatOps.(
sqrt((x * x) + (y * y))
);
但是否真的需要在实际项目中这样做,这个问题值得商榷。
导入模块的另一种方式是include,这样该模块所有导出值和方法都将添加到当前的模块中,一起被导出。这类似于面向对象编程中类之间的继承。 在以下示例中,模块LogWithDate是模块Log的扩展。它除了具有所有Log的方法外,还有logStrWithDate()这个新功能。
/* LogWithDateMain.re */
module LogWithDate = {
include Log;
let logStrWithDate = (str: string, log: t) => {
let dateStr = Js.Date.toISOString(Js.Date.make());
logStr("[" ++ dateStr ++ "] " ++ str, log);
};
};
let () = LogWithDate.(
make()
|> logStrWithDate("Hello")
|> logStrWithDate("everyone")
|> print
);
Js.Date 来自BuckleScript的标准库,就不在这里解释了。你可以随意的include模块,不受数量限制。
通过下面的方法 include 接口(InterfaceB继承InterfaceA):
module type InterfaceA = {
···
};
module type InterfaceB = {
include InterfaceA;
···
}
与模块类似,你可以随意include接口。 我们来为模块LogWithDate创建一个接口。可惜,我们不能通过名字直接includeLog的接口,因为它没有。但是,我们可以通过它的模块(A行)间接引用它:
module type LogWithDateInterface = {
include (module type of Log); /* A */
let logStrWithDate: (t, t) => t;
};
module LogWithDate: LogWithDateInterface = {
include Log;
···
};
你只是给导入模块取一个别名,而不是真正的改变他的名字。 给模块取别名:
module L = List;
这样给模块的方法取别名:
let m = List.map;
在大型项目中,ReasonML识别模块的方式可能会出现问题。由于它具有单个全局模块名称空间,因此可能很容易出现名称冲突。比方说,可能在两个不同的目录中存在两个不同的Util模块。 这是可以使用命名空间模块。以下面的项目为例:
proj/
foo/
NamespaceA.re
NamespaceA_Misc.re
NamespaceA_Util.re
bar/
baz/
NamespaceB.re
NamespaceB_Extra.re
NamespaceB_Tools.re
NamespaceB_Util.re
在这个项目中有两个Util模块,但他们的标识是不同的,因为他们分别有NamespaceA和NamespaceB的前缀:
proj/foo/NamespaceA_Util.re
proj/bar/baz/NamespaceB_Util.re
为了减少命名的难度,每个命名空间都有一个命名空间模块。第一个看起来像这样:
/* NamespaceA.re */
module Misc = NamespaceA_Misc;
module Util = NamespaceA_Util;
NamespaceA 用法如下:
/* Program.re */
open NamespaceA;
let x = Util.func();
全局open使我们可以让我们不加前缀的使用Util。 还有两个用法: 覆盖模块,甚至是标准库中的模块。例如,NamespaceA.re可以包含一个自定义List的实现,它将覆盖内置模块Program.re中的List:
module List = NamespaceA_List;
创建嵌套模块,并将子模块保存在单独的文件中。例如,除了open NamespaceA之外,你也可以通过访问NamespaceA.Util来访问Util,因为它嵌套在里面NamespaceA。当然NamespaceA_Util,也是可以的,但是不鼓励,因为它是一个底层实现。 这还被BuckleScript用于Js.Date,Js.Promise等等,在文件中js.ml(这是OCaml语法):
···
module Date = Js_date
···
module Promise = Js_promise
···
module Console = Js_console
命名空间模块在Jane Street的OCaml中广泛使用。他们称它们为打包模块,但我更喜欢命名空间模块,因为它不会与npm term 软件包冲突。 本节的源代码:“Better namespaces through module aliases” by Yaron Minsky for Jane Street Tech Blog。
ReasonML的标准库有两个重要的注意事项:(译者注:明明是三点) 标准库还在持续更新。 内置模块的命名风格将从下滑线(foo_bar和Foo_bar)变成驼峰式(fooBar和FooBar)。 目前,功能任不完善。
ReasonML对标准库做了拆分:大多数ReasonML核心的API都可以在native和JavaScript上运行(通过BuckleScript)。如果你编译为JavaScript,则需要在两种情况下使用BuckleScript的API: ReasonML的API功能不并完善。例如对日期操作的支持,你需要借助BuckleScript的Js.Date 。 BuckleScript不支持的功能。包括模块Str(由于JavaScript的字符串与ReasonML的原生字符串不同)和Unix(使用原生APIs)。 这是两个API的文档: ReasonML API文档 BuckleScript API文档
Pervasives模块包含核心标准库,并且默认为每个模块自动open。它包含的功能,如操作符==,+,|>和诸如print_string()和string_of_int()这样的函数。 如果此模块中的某些内容被覆盖,你仍然可以显式的访问它Pervasives.(+)。 如果项目中有一个文件Pervasives.re,它会覆盖内置模块,而不是打开。
以下模块存在两种版本:一种较旧的模块,其中函数只有位置参数;而较新的模块,支持参数别名。 Array, ArrayLabels Bytes, BytesLabels List, ListLabels String, StringLabels 例如:
List.map: ('a => 'b, list('a)) => list('b)
ListLabels.map: (~f: 'a => 'b, list('a)) => list('b)
另外两个模块提供了函数的别名: Module StdLabels的子模块Array,Bytes,List,String,都是ArrayLabels的别名。在你的模块里,你可以openStdLabels模块来默认使用有别名版本的List。 Module StdLabels还有三个带有别名函数的子模块:Hashtbl,Map和Set。
目前,JavaScript是ReasonML的首选平台。因此,安装库的首选方式是npm。例子如下,假设我们想为Jest安装BuckleScript绑定(包括Jest本身)。相关的npm包是bs-jest。 首先,我们需要install package。package.json:
{
"dependencies": {
"bs-jest": "^0.1.5"
},
···
}
其次,我们需要把包添加到bsconfig.json:
{
"bs-dependencies": [
"bs-jest"
],
···
}
之后,我们可以用Jest.describe()了。
安装库更多相关的信息: BuckleScript’s build system is explained in Chap. “Build system support” of the BuckleScript Manual. ReasonML’s manual explains how to find ReasonML libraries on npm. Useful npm keywords include: reason, reasonml, bucklescript
创建你自己的ReasonML项目:Sect. “Template projects” in “Getting started with ReasonML”.
扫码关注w3ctech微信公众号
共收到0条回复