跳至主要內容

Nix 语言快速入门

NixOS-CN大约 24 分钟

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 分配给值 20\dfrac{2}{0} ,这是一种典型的数学错误:

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),只有 truefalse 两种
  • 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

注意语法细节:

  • letin 之间的“名称-值”对以 ; 结尾;
  • 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 manualopen in new window 中对 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

小结

本小节给出了属性访问的两种应用场景,第一种是获取属性的值,第二种是为值分配属性名称。

显然,第二种场景不是必须使用属性访问的写法,它只是更方便。仅就这个场景来看,这是一种语法糖

我们将在下一节介绍另外两种常用的语法糖。

语法糖 withinherit

语法糖(syntactic sugar)是对语言功能没有影响,但更方便使用的一种语法。本节将介绍两种常用的语法糖 withinherit

with 表达式

with 能简化特定形式的列表。

  • 举个例子,列表 [ a.x a.y ] 中出现了两次 a
  • with 表达式就可以帮助你简化这种列表,将其写作 with a; [ x y ]
  • 注意语法细节:a 后面有一个分号 ;,而 with 表达式的作用域为分号后的第一个列表。

在下面的例子中,由于 [ a.x a.y ] 等价于 with a; [ x y ];R1R2 的值一致。

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_PATHopen in new window 的路径值:

/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; }

函数库

除了一些内建操作符open in new window+, ==, &&, 等),我们还要学习一些被视为事实标准的库。

内建函数

它们在 Nix 语言中并不是 <LAMBDA> 类型,而是 <PRIMOP> 元操作类型(primitive operations)。这些函数是内置在 Nix 解释器中,由 C++ 实现。查询内建函数open in new window 以了解其使用方法。

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