Nix 语言快速入门
Nix 语言快速入门
基础要求
以下教程需要你具有一定基础。具体来说,如果你已经知道在编程领域什么是变量(variable)、字符串(string)、函数(function)及参数(argument),那么你的知识水平就差不多足够了。
仅面向本文维护者的说明,单击以切换折叠/展开
本文在设计上是线性的,也即只需要读者具备一点点基础,就可以通过按顺序从头读到尾的方式完成本文的学习。因此,请留心说明顺序,例如讲 let 绑定时如果举了一个列表的例子,你需要确保前面已经正式介绍过列表。再如,讲 with 语法糖的时候同时用到 let 绑定和列表,那么这两个概念都需要在前面已经正式介绍过。否则,读者很可能会面对初次接触的语法或者概念而被卡住,这会严重影响学习效率甚至是完成率。若出于顺序安排的其他合理性原因,实在无法避开在说明中涉及陌生概念,可以提示读者相关部分不需要理解,后面会讲到。Nix 作为语言,是一门简单的函数式语言,它被专门设计并用于 Nix 包管理器及相关生态(NixOS、Home-Manager 等)。
实践环境
在学习 Nix 语言时,虽然不是必须,但若动手实践,效率往往会高得多。
以下给出两种实践方法。这不是必须的,你也可以跳过本节。
注意
本节需要你已经安装了 Nix 或正在使用 NixOS。
另外,本教程中的示例代码并不全是为了供直接运行而写的。对于每一段代码,若想实践其效果,请先理解对应的知识,再基于这段代码自己编写测试代码以运行。
(对于 Nix 来说,运行代码被称为求值(evaluate),而只有表达式(expression)能被求值;但是,示例的代码未必是表达式,而可能是属性集的元素等。)
交互模式
你可以通过在命令行运行
nix repl
进入交互模式,其界面类似下面的样子:
Nix 2.31.1
Type :? for help.
nix-repl>
此时输入表达式,例如
1 + 2
回车即可求值,得到结果如下:
3
提示
输入 :q
可以退出交互模式。
文件求值
交互模式简单快捷,但我们平时使用 Nix 语言进行编辑配置、打包等操作时,大多数情况下不会直接使用交互模式,而是对 *.nix
纯文本文件进行编辑。
因此,如果你习惯于使用编辑器,这里更推荐利用文件求值进行实践。每个 nix 文件的内容都是一个表达式,这是 nix 文件能被求值的前提。
例如,新建文件 foo.nix
,将其内容编辑如下:
1 + 2
保存后,在命令行运行
nix-instantiate --eval foo.nix
结果如下:
3
拓展说明:求值的惰性与嵌套迭代
此部分内容较长,仅供有兴趣的人阅读。
单击以切换折叠/展开
Nix 的求值具有惰性(laziness),只会在有必要时进行。例如,下述代码(看不懂没关系)将名称 a
分配给值 ,这是一种典型的数学错误:
let a = builtins.div 2 0; b = 3; in b
运行它所得结果为 3
,竟然不会报错?实际上,这正是因为 a
的值不被需要(只需要输出 b
的值),所以也未被求值。
与惰性类似的是另一种行为是,嵌套属性集的求值,在交互模式和文件求值模式下,除非必要,默认不会迭代,而是以占位符替代,例如
{ a.b.c = 1; }
使用交互模式,结果如下
{
a = { ... };
}
使用文件求值的结果如下
{ a = <CODE>; }
不过,与惰性不同,迭代求值的行为可以直接控制。
- 交互模式:在开头添加
:p
,例如:p { a.b.c = 1; }
。 - 文件求值:添加
--strict
参数,例如nix-instantiate --eval --strict foo.nix
。
结果如下:
{
a = {
b = { c = 1; };
};
}
好了,下面正式介绍 Nix 语法。
注释、缩进与换行
注释、缩进与换行的语法与机制,对编程语言的风格有重要影响。本节将介绍 Nix 语言中的注释、缩进与换行。
- 注释:在 Nix 语言中,用
#
表示注释,在它之后直到行末的部分都会被忽略。 - 缩进与换行:与 Python 这种对缩进有要求的语言不同,在 Nix 语言中,大多数情况下,换行与缩进只是为了更好的可读性,并不影响代码的本质。
例如,下面的两段示例代码(你目前还不需要理解它们的含义),它们在本质上(也即在 Nix 解释器看来)并没有区别。
第一例:
{ a = 1; b = 2; }
第二例:
{ # 这是一句注释,放在代码所在行的末尾。
# 这也是一句注释,单独占了一行。
a = 1; # 这一行即使不缩进,也不影响代码本质。
b = 2;
# c = 3; # 这里的代码被注释掉了,相当于不存在。
}
注意
换行与空格一样具有分隔作用,请勿在不可分隔的地方胡乱断行。
名称与属性集
变量是大多数编程语言中最基础的概念,而与之类似的名称则是 Nix 语言中最基础的概念。本节将会介绍 Nix 中如何将名称分配给值,以及最常用的数据类型——属性集,继而引出递归属性集与列表的概念。
名称和值
我们可以使用 =
将名称分配给值,形成“名称 - 值”对。例如将名称 foo
分配给值 123
:
foo = 123
相关信息
上面的示例不属于表达式(但可以作为表达式的一部分),所以你无法将它直接写入 nix 文件进行文件求值;不过 nix repl
有一些灵活的处理,允许你输入这样的结构。
函数式语言与命令式语言中“变量”的区别
太长不看版:在 Nix 里,“名称”就是“变量”,只是这个变量一旦绑定便成永恒;它保留了数学“可取不同值”的语义,却丢掉了命令式“可重新赋值”的含义。
维度 | 命令式语言 | Nix(函数式) |
---|---|---|
底层模型 | 存储格(内存单元) | 无存储格,只有「名称-值」映射 |
操作 | 赋值:随时把新值写回同一单元 | 绑定:一次性把名称贴到值,不可重写 |
所谓“变量” | 存储格的别名 → 之后可反复擦写 | 数学意义上的变量 → 同一作用域内值固定 |
文档用词 | variable = 可重写的存储格 | variable = 一次性绑定的名称(不会变) |
名称的值并不仅限于 123
这种整数。具体来说有以下数据类型(不需要完全理解,留下印象即可)
- 字符串(string),例如
"Hello world"
- 整数(integer),例如
1
- 浮点数(float),例如
3.141
- 布尔(bool),只有
true
与false
两种 - null,只有
null
一种 - 列表(list),例如
[ 1 "tux" false ]
- 属性集(attribute set),例如
{ a = 1; b = "tux"; c = false; }
属性集
在 Nix 语法中,属性集是最常见的数据类型之一,基本示例如下:
{
a = 1;
b = 2;
}
概念说明:
- 属性集(attribute set)就是装载若干对名称与值的集合。
- 属性集内的名称被称为这个属性集的属性(attribute);
- 属性集内由名称和值组成的对被称为该属性的元素(element);
语法说明:
- 属性集以
{
}
为边界,其内部为多个“名称-值”对,且它们末尾必须添加;
。
上述代码将 foo
的值定义为属性集 { a = 1; b = 2; }
,因此可称之为属性集 foo
。
属性集 foo
中有两个属性:
- 属性
a
,其值为1
- 属性
b
,其值为2
属性的值除了可以是 1
2
这样的数值外,也可以是一个属性集(也即支持嵌套),例如将 b
的值改为属性集 { c = 2; d = 3; }
:
{
a = 1;
b = {
c = 2;
d = 3;
};
}
嵌套属性集中的属性也可以利用 .
表示,例如上面这段的一种等价写法如下:
{
a = 1;
b.c = 2;
b.d = 3;
}
提示
上面的写法被称为“属性访问”,后面会再次介绍。
递归属性集
普通的属性集不支持递归引用,举个例子:
{
a = 1;
b = a + 1;
}
对上面的表达式求值,会报错:
error: undefined variable 'a'
可见,当属性集内的属性 b
需要访问该属性集的另一个属性 a
时,即使 a
是“先”定义的,也无法访问到。此时就需要我们改用递归(recursive)属性集,它相比普通的属性集,在前面多加了 rec
:
rec {
a = 1;
b = a * 2 + 1;
}
对上面的表达式求值,结果如下:
{ a = 1; b = 3; }
求值结果的排序依据
可以看到,结果中的 a = 1
在前面,b = 3
在后面。这种顺序实际上与任何其它因素(包括声明顺序、求值依赖关系)都无关,而只与属性名称本身的排序有关。例如,对 rec { a = 1; b = 2; }
与 rec { b = 2; a = 1; }
的求值,都会把 a = 1
放在前面,归因到底,只是 a
在字母表中位于 b
之前罢了。(直接原因则与 Nix 解释器对名称排序所用到的算法或者调用的库有关,这里不再深入。)
求值过程的顺序机制
既然求值结果的排序与求值顺序等因素无关,那么求值顺序由什么决定呢?
将刚才例子中属性集里的两个元素位置对调:
rec {
b = a * 2 + 1;
a = 1;
}
你会发现,Nix 解释器能自动处理求值顺序,并不会因为 a
的声明被调整到后面而影响求值结果(与之前的完全一致,从略)。这看起来相当“智能”,你甚至可以写得更复杂一些,比如 Nix 解释器也能自动处理下面的例子(结果略):
rec {
c = a * 2 - b + d - 35;
a = 12;
b = d * 2 + 64;
d = a - 15;
}
不过,这并不代表你可以直接用它来解方程。例如我们再写一个在数学上有唯一解的方程组:
rec {
b = a * 2 + 1;
a = b + 1;
}
此表达式求值的输出如下:
{
a = «error: infinite recursion encountered»;
b = «error: infinite recursion encountered»;
}
由此可见,递归属性集内部处理求值顺序的机制,确实是递归的,而如果递归陷入死循环就会报错。
列表
之前我们学习了属性集,它含有多个元素,例如:
{
a = "apple";
b = "orange";
c = "banana";
}
上面的名称 a
b
c
或许可以有明确的含义,但有些场景不需要这些名称,而只关心后面的值,这种情况下就可以使用列表,例如:
[ "apple" "orange" "banana" ]
需要注意语法细节:
- 列表以
[
]
为边界,其内部为多个元素,每个元素都是值(value)。 - 元素之间使用空格(或换行)分隔,各元素不以
;
结尾。
let 绑定与属性访问
前面关于名称的使用是非常基本的,我们还需要更灵活的处理方法。本节将会介绍另一种将名称分配给值的方法——let 绑定,以及风格简洁的属性访问。
let
绑定
有时我们希望在指定的范围内为值分配名称,此时就可以使用 let
绑定,示例如下:
let
a = 1;
b = 2;
in
a + b # 结果是 3
注意语法细节:
let
与in
之间的“名称-值”对以;
结尾;in
之后只有一个表达式。注意,这只是语法形式上的要求,并不代表let
绑定的用处很有限,因为表达式本身可以很复杂,常见的是嵌套属性集。作为基本示例,下面演示刚刚学到的列表:
let
b = a + 1;
c = a + b;
a = 1;
in
[ a b c ]
求值的结果如下:
[ 1 2 3 ]
作用域
let
绑定是有作用域的,绑定的名称只能在作用域使用,或者说每个 let
绑定的名称只能在该表达式内使用。例如下面的例子:
{
a = let x = 1; in x;
b = x;
}
由于 b = x;
不在作用域之内,会有报错如下:
error: undefined variable 'x'
局部变量(?)
Nix 中不存在“全局变量”,因而“局部变量”的说法可能引起误会,应当尽量避免使用。
不过,Nix manual 中对 let 绑定的介绍提到了局部变量(local variable)。
A let-expression allows you to define local variables for an expression.
这种说法可能不合适,但既然官方文档也有用到,其他地方自然也可能会出现,留心即可。
属性访问
前面提到,嵌套属性集中的属性可以利用 .
表示,这被称为属性访问(attribute access)。
在下面这个例子中,我们定义了一个嵌套属性集 a
,并使用 a.b.c
访问值 123
:
let
a = { b = { c = 123; }; };
in
a.b.c
求值,结果如下:
123
利用访问属性的写法,可以更加方便地为值分配名称。比如,上面的例子也可以这样写(返回结果不变):
let
a.b.c = 123;
# 还可以写成下面这样
# a = { b.c = 123; };
# 再或者这样
# a.b = { c = 123; };
in
a.b.c
小结
本小节给出了属性访问的两种应用场景,第一种是获取属性的值,第二种是为值分配属性名称。
显然,第二种场景不是必须使用属性访问的写法,它只是更方便。仅就这个场景来看,这是一种语法糖。
我们将在下一节介绍另外两种常用的语法糖。
语法糖 with
和 inherit
语法糖(syntactic sugar)是对语言功能没有影响,但更方便使用的一种语法。本节将介绍两种常用的语法糖 with
和 inherit
。
with
表达式
with
能简化特定形式的列表。
- 举个例子,列表
[ a.x a.y ]
中出现了两次a
。 with
表达式就可以帮助你简化这种列表,将其写作with a; [ x y ]
。- 注意语法细节:
a
后面有一个分号;
,而with
表达式的作用域为分号后的第一个列表。
在下面的例子中,由于 [ a.x a.y ]
等价于 with a; [ x y ];
,R1
和 R2
的值一致。
let
a = {
x = 1;
y = 2;
};
in
{
R1 = [ a.x a.y ];
R2 = with a; [ x y ];
}
进行严格求值,返回结果:
{ R1 = [ 1 2 ]; R2 = [ 1 2 ]; }
就近性
不过,这种等价并不是恒定的,比如下面的例子,我们在 let
后面直接加一行 x = 0;
:
let
x = 0;
a = {
x = 1;
y = 2;
};
in
{
R1 = [ a.x a.y ];
R2 = with a; [ x y ];
}
进行严格求值,返回结果:
{ R1 = [ 1 2 ]; R2 = [ 0 2 ]; }
可见,with
表达式具有一种“就近性”,当 x
的值可以不经嵌套地直接访问时,它会直接返回这个值,而不会使用 a
来嵌套地访问。
inherit
语法
首先说明什么是“继承”。
例如下面的表达式:
let
a = 1;
b = 2;
x = 3;
y = 4;
in
{
m = a;
n = b;
x = x;
y = y;
}
求值结果:
{ m = 1; n = 2; x = 3; y = 4; }
在此例中,
m
获取了a
的值,n
获取了b
的值。- 而
x
y
则直接从同名变量获取值,这被称为“继承”(inherit)。
下面将要介绍的 inherit
语法,则简化了这种继承所需的“名称-值”对。比如,刚才的例子可以这样写(求值结果不变):
let
a = 1;
b = 2;
x = 3;
y = 4;
in
{
m = a;
n = b;
inherit x y;
# 也可以分开写
# inherit x;
# inherit y;
}
注意
inherit
的语法结构,例如上面的 inherit x;
,本质上仍然属于“名称-值”对,不属于表达式。
inherit
还支持前置一对括号 ()
包裹属性集,实现属性访问的效果。例如下面的例子:
let
a = { x = 1; y = 2; };
in
{
inherit (a) x y;
# 等价于
# x = a.x;
# y = a.y
# 注:这里没有列表,不要和 with 混淆。
}
严格求值,结果如下:
{ x = 1; y = 2; }
利用 inherit 提升“嵌套级别”
由于 inherit
可实现这种属性访问的效果,它的用法还可以更灵活。比如下面的例子:
let
a = { x = 1; y = 2; };
in
with a; [ x y ]
严格求值,结果如下:
[ 1 2 ]
而如果利用 inherit
我们还可以这样写:
let
inherit ({ x = 1; y = 2; }) x y;
# 等价于
# x = { x = 1; y = 2; }.x;
# y = { x = 1; y = 2; }.y;
in
[ x y ]
对其严格求值的结果不变。可见,利用 inherit
,我们变相地提升了 { x = 1; y = 2; }
中属性的“嵌套级别”,在后续代码中得以省去属性访问。
文件系统路径
在 Nix 语言中,文件系统路径(file system paths;简称路径)是一种数据类型,它不同于后面要介绍的字符串类型。
路径的基本语法
路径有绝对路径(absolute path)和相对路径(relative path)两种,它们都必须满足:
- 路径至少包含一个
/
。- 对于相对路径,若目标已经在当前目录,可前置
./
。
- 对于相对路径,若目标已经在当前目录,可前置
- 路径不能以
/
结尾。- 若有需要,可在
/
后加.
。
- 若有需要,可在
注意
因路径的语法不正确导致的报错,看起来可能会很奇怪,留心即可。
例如把当前路径 ./.
错写成 .
:
.
求值报错:
error: syntax error, unexpected '.'
绝对路径以 /
开头。
例一:/etc/os-release
文件
/etc/os-release
- 不能写成
/etc/os-release/
- 注意,这不是因为
os-release
属于文件而非目录,而是因为路径不能以/
结尾。
- 注意,这不是因为
- 可以写成
/etc/os-release/.
求值结果:
/etc/os-release
例二:根目录
/.
- 不能写成
/
(路径不能以/
结尾)
求值结果:
/
相对路径不以 /
开头,且求值结果与当前所在目录(以下假设 /home/user
)有关。
例一:当前目录(用 .
表示):
./.
- (虽然一般不合适)还可以写成
././././.
- 但是不能写成
.
(缺少/
,不构成路径)
求值结果:
/home/user
例二:当前目录下的 Downloads
./Downloads
- 也可以写成
Downloads/.
- 但是不能写成
Downloads
(缺少/
,不构成路径) - 也不能写成
Downloads/
(路径不能以/
结尾)
求值结果:
/home/user/Downloads
例三:用 ..
指定上级目录
../../etc
- 也可以写成
./../../etc
求值结果:
/etc
检索路径
检索路径(lookup paths)又名“尖括号语法”(angle bracket syntax),是通过系统变量来获取路径的语法。其最简单的形式是以一对尖引号 <
>
包裹所需内容。
例如:
注意
请不要急着运行下面的示例,因为它实际包含更多内容。
<nixpkgs>
这个时候 <nixpkgs>
实际上依赖了系统变量中一个名为$NIX_PATH
的路径值:
/nix/var/nix/profiles/per-user/root/channels/nixpkgs
注意
我们建议你避免使用检索路径来指定其它相对路径,比如下面的例子:
<nixpkgs/lib>
这是一种污染,因为这样指定相对路径会让配置与环境产生联系。我们的配置文件应该尽量保留纯函数式的特性,即输出只与输入有关,纯函数不应该与外界产生任何联系。
字符串
字符串(string)是一种常见的数据类型,其最简单的形式是以一对双引号 "
"
包裹所需内容。
例如:
"hello world!"
字符串插值
字符串插值,这个功能是各大流行语言的标配。
在 Nix 中,使用 "${ ... }"
可以插入名称的值:
let
name = "Nix";
in
"hello ${name}"
输出为:
"hello Nix"
名称的值的数据类型
字符串插值语法支持的值必须为字符串类型,或是可以转换为字符串的数据类型。例如:
let
x = 1;
in
"${x} + ${x} = ${x + x}"
由于 x
的值为数字类型,对此求值的报错如下:
# ... 前面略
error: cannot coerce an integer to a string
如果确实需要将数字作为插值参数,应该怎么办呢?
虽然有点早,提前告诉你—— 对于非字符串类型,可以显式使用内置函数 toString
,将其转换为字符串类型:
let
x = 1;
in
"${toString x} + ${toString x} = ${toString (x + x)}"
求值的结果如下:
"1 + 1 = 2"
字符串插值也支持嵌套,例如:
let
a = "pen";
b = "apple";
c = "pineapple";
in
{
# 请注意 plus 和 equals 两侧留出的空格
# 对比下面两行,它们的值完全一致
L1="${a + " plus ${b + " equals ${c}"}"}.";
L2="${a+" plus ${b+" equals ${c}"}"}.";
}
求值结果如下:
{
L1 = "pen plus apple equals pineapple.";
L2 = "pen plus apple equals pineapple.";
}
多行字符串
有时我们需要用字符串表示多行内容,此时可利用转义,将 \n
作为换行符。
比如对于以下内容:
Please run
cat /etc/os-release
to get distro info.
可用字符串表示为
"Please run\n cat /etc/os-release\nto get distro info.\n"
求值结果如下:
"Please run\n cat /etc/os-release\nto get distro info.\n"
提示
上面的求值结果看起来仍然不是多行的,但其实从数据本身内容来说是没有问题的。
如果想要渲染出多行的样子,文件求值时可以加 --raw
参数,比如 nix-instantiate --eval --raw foo.nix
,结果如下:
Please run
cat /etc/os-release
to get distro info.
而若使用 nix repl
则需要其它方法来达成目的,这里不再展开。
但是,这样做的可读性较差,可维护性也不好。
一个更合适的方法是使用缩进字符串(indented strings),也称为多行字符串(multi-line strings)。
其基本形式为,用两组 ''
作为开头和结尾,中间包裹所需内容。
比如刚才的例子等价于:(求值结果不变)
''
Please run
cat /etc/os-release
to get distro info.
''
智能去除缩进
Nix 的多行字符串会统一去除开头的缩进,这在其他语言中是不常见的。
比如刚才的例子还等价于:(求值结果不变)
''
Please run
cat /etc/os-release
to get distro info.
''
字符串中的字符转义
在单行字符串中, Nix 的转义语法与许多其他语言相同, "
\
${
以及其他 \n
\t
等特殊字符,都可直接使用 \
进行转义。
比如,内容 this is a "string" \
可用下面的代码表示:
"this is a \"string\" \\"
但在多行字符串中,不是使用 \
,而是使用 ''
来转义。
比如,下面的例子会输出原始字符 ${a}
,而不是做字符串插值:
let
a = "1";
in
''the value of a is:
''${a}
''
求值结果如下:
"the value of a is:\n \${a}\n"
其他 \n
\t
等特殊字符的转义也类似,必须使用两个单引号来转义,如
''
this is a
multi-line
string
''\n
''
但如果我们希望在字符串中使用原始字符 ''
,因为会与多行字符串原有的语义冲突,不能直接写 ''
,而必须改用 '''
三个单引号。
也就是说,在多行字符串中的 '''
三个单引号这样的组合,实际输出的是原始字符串 ''
.
举个例子:
let
a = "1";
in
''the value of a is:
'''${a}'''
''
求值结果如下:
"the value of a is:\n ''1''\n"
函数
函数在 Nix 语言中是人上人,我们先来声明一个匿名函数(Lambda):
x: x + 1
冒号左边是函数参数,冒号右边跟随一个空格,随即是函数体。
这是个嵌套的函数,支持多重参数(柯里化函数):
x: y: x + y
参数当然可以是属性集类型:
{ a, b }: a + b
为函数指定默认参数,在缺少该参数的值的情况下,它就是默认值:
{ a, b ? 0 }: a + b
允许传入额外的属性:
{ a, b, ...}: a + b # 明确传入的属性有 a 和 b,传入额外的属性将被忽略
{ a, b, ...}: a + b + c # 即使传入的属性有 c,一样不会参与计算,这里会报错
为额外的参数绑定到参数集,然后调用:
args@{ a, b, ... }: a + b + args.c
{ a, b, ... }@args: a + b + args.c # 也可以是这样
为函数命名:
let
f = x: x + 1;
in
f
调用函数,并使用函数构建新属性集:
concat = { a, b }: a + b # 等价于 concat = x: x.a + x.b
concat { a = "Hello "; b = "NixOS"; }
输出:
Hello NixOS
由于函数与参数使用空格分隔,所以我们可以使用括号将函数体与参数分开:
(x: x + 1) 1 # 向该 Lambda 函数传入参数 1
柯里化函数
我们将 f (a,b,c)
转换为 f (a)(b)(c)
的过程就是柯里化。为什么需要柯里化?因为它很灵活,可以避免重复传入参数,当你传入第一个参数的时候,该函数就已经具有了第一个参数的状态(闭包)。
尝试声明一个柯里化函数:
x: y: x + y
为了更好的可读性,我们推荐你这样写:
x: (y: x + y)
这个例子中的柯里化函数,虽然接收两个参数,但不是"迫切"需要:
let
f = x: y: x + y;
in
f 1
输出为:
<LAMBDA>
f 1
的值依然是函数,这个函数大概是:
y: 1 + y;
我们可以保存这个状态的函数,稍后再来使用:
let
f = x: y: x + y;
in
let g = f 1; in g 2
也可以一次性接收两个参数:
let
f = x: y: x + y;
in
f 1 2
属性集参数
当我们被要求必须传入多个参数时,使用这种函数声明方法:
{a, b}: a + b
调用该函数:
let
f = {a, b}: a + b;
in
f { a = 1; b = 2; }
如果我们额外传入参数,会怎么样?
let
f = {a, b}: a + b;
in
f { a = 1; b = 2; c = 3; }
意外参数 c
:
error: 'f' at (string):2:7 called with unexpected argument 'c'
at «string»:4:1:
3| in
4| f { a = 1; b = 2; c = 3; }
| ^
5|
默认参数
前面稍微提到过一点,没有什么需要过多讲解的地方:
let
f = {a, b ? 0}: a + b;
in
f { a = 1; }
传入参数值是可选的,根据你的需要来:
let
f = {a, b ? 0}: a + b;
in
f { a = 1; b = 2; }
额外参数
有的时候,我们设计的函数不得不接收一些我们不需要的额外参数,我们可以使用 ...
允许接收额外参数:
{a, b, ...}: a + b
不比上个例子,这次不会报错:
let
f = {a, b, ...}: a + b;
in
f { a = 1; b = 2; c = 3; }
命名参数集
又名 "@
模式"。在上文中,我们已经可以接收到额外的参数了,假如我们需要使用某个额外参数,我们可以使用命名属性集将其接收到一个另外的属性集:
{a, b, ...}@args: a + b + args.c # 这样声明函数
args@{a, b, ...}: a + b + args.c # 或是这样
具体示例如下:
let
f = {a, b, ...}@args: a + b + args.c;
in
f { a = 1; b = 2; c = 3; }
函数库
除了一些内建操作符 (+
, ==
, &&
, 等),我们还要学习一些被视为事实标准的库。
内建函数
它们在 Nix 语言中并不是 <LAMBDA>
类型,而是 <PRIMOP>
元操作类型(primitive operations)。这些函数是内置在 Nix 解释器中,由 C++ 实现。查询内建函数 以了解其使用方法。
builtins.toString() # 通过 builtins 使用函数
导入
import
表达式以其他 Nix 文件的路径为参数,返回该 Nix 文件的求值结果。
import
的参数如果为文件夹路径,那么会返回该文件夹下的 default.nix
文件的执行结果。
如下示例中,import
会导入 ./file.nix
文件,并返回该文件的求值结果:
$ echo 1 + 2 > file.nix
import ./file.nix
3
被导入的 Nix 文件可以返回任何内容,返回值可以向上面的例子一样是数值,也可以是属性集(attribute set)、函数、列表,等等。
如下示例导入了 file.nix
文件中定义的一个函数,并使用参数调用了该函数:
$ echo "x: x + 1" > file.nix
import ./file.nix 1
2