diff --git a/src/tutorials/lang/QuickOverview.md b/src/tutorials/lang/QuickOverview.md index 20e7bb49..24c739f2 100644 --- a/src/tutorials/lang/QuickOverview.md +++ b/src/tutorials/lang/QuickOverview.md @@ -11,7 +11,7 @@ :::
仅面向本文维护者的说明,单击以切换折叠/展开 -本文在设计上是线性的,也即只需要读者具备一点点基础,就可以通过按顺序从头读到尾的方式完成本文的学习。因此,请留心说明顺序,例如讲 let 绑定时如果举了一个列表的例子,你需要确保前面已经正式介绍过列表。再如,讲 with 语法糖的时候同时用到 let 绑定和列表,那么这两个概念都需要在前面已经正式介绍过。否则,读者很可能会面对初次接触的语法或者概念而被卡住,这会严重影响学习效率甚至是完成率。若出于顺序安排的其他合理性原因,实在无法避开在说明中涉及陌生概念,可以提示读者相关部分不需要理解,后面会讲到。 +本文在设计上是线性的,也即只需要读者具备一点点基础,就可以通过按顺序从头读到尾的方式完成本文的学习。因此,请留心说明顺序,例如讲 let 绑定时如果举了一个列表的例子,你需要确保前面已经正式介绍过列表。再如,讲 with 语法糖的时候同时用到 let 绑定和列表,那么这两个概念都需要在前面已经正式介绍过。否则,读者很可能会面对初次接触的语法或者概念而被卡住,这会严重影响学习效率甚至是完成率。若出于顺序安排的其他合理性原因,实在无法避开在说明中涉及陌生概念,可以提示读者相关部分不需要理解,后面会讲到。另外,常用的 callout 块中,info 显示为蓝色,适用于普通知识点;而 note 显示为灰色,适用于“就算不理解也没关系”的高阶或补充知识。至于 tip 和 warning,它们有可能涉及到知识,也可能不涉及,重点区别在于 tip 偏向于“实用建议/能帮助理解或加强记忆的提示”,而 warning 偏向于“能够避免出现问题或损失的提示”(包括时间精力方面的)以及“注意避开误区的说明”。
Nix 作为语言,是一门简单的函数式语言,它被专门设计并用于 Nix 包管理器及相关生态 @@ -27,12 +27,11 @@ Nix 作为语言,是一门简单的函数式语言,它被专门设计并用 ::: warning 本节需要你已经安装了 Nix 或正在使用 NixOS。 -另外,本教程中的示例代码并不全是为了供直接运行而写的。对于每一段代码,若想实践其 -效果,请先理解对应的知识,再基于这段代码自己编写测试代码以运行。 - -(对于 Nix 来说,运行代码被称为**求值**(evaluate),而只有**表达 -式**(expression)能被求值;但是,示例的代码未必是表达式,而可能是属性集的元素 -等。) +另外,本教程中的示例代码并不全是为了供直接运行而写的。 +对于 Nix 来说,运行代码被称为**求值**(evaluate), +而只有**表达式**(expression)能被求值; +但是,示例的代码未必是表达式,此时若仍想进行测试, +则需要你基于此示例,自己编写测试代码。 ::: @@ -74,10 +73,11 @@ nix-repl> ### 文件求值 -交互模式简单快捷,但我们平时使用 Nix 语言进行编辑配置、打包等操作时,大多数情况 -下不会直接使用交互模式,而是对 `*.nix` 纯文本文件进行编辑。 +交互模式简单快捷,但我们平时使用 Nix 语言进行编辑配置、打包等操作时, +大多数情况下不会直接使用交互模式,而是对 `*.nix` 纯文本文件进行编辑。 -因此,如果你习惯于使用编辑器,这里更推荐利用文件求值进行实践。每个 nix 文件的内容都是**一个**表达式,这是 nix 文件能被求值的前提。 +因此,如果你习惯于使用编辑器,这里更推荐利用文件求值进行实践。 +每个 nix 文件的内容都是**一个**表达式,这是 nix 文件能被求值的前提。 例如,新建文件 `foo.nix`,将其内容编辑如下: @@ -98,7 +98,7 @@ nix-instantiate --eval foo.nix ``` -::: info 拓展说明:求值的惰性与嵌套迭代 +::: note 拓展说明:求值的惰性与嵌套迭代 此部分内容较长,仅供有兴趣的人阅读。 @@ -174,10 +174,10 @@ let a = builtins.div 2 0; b = 3; in b 第二例: ```nix -{ # 这是一句注释,放在代码所在行的末尾。 - # 这也是一句注释,单独占了一行。 +{ # 这是一句注释,放在代码所在行的末尾。 +# 这也是一句注释,单独占了一行。 a = 1; # 这一行即使不缩进,也不影响代码本质。 - b = 2; + b = 2; # c = 3; # 这里的代码被注释掉了,相当于不存在。 } ``` @@ -190,30 +190,41 @@ let a = builtins.div 2 0; b = 3; in b ::: ## 名称与属性集 -变量是大多数编程语言中最基础的概念,而与之类似的名称则是 Nix 语言中最基础的概念。本节将会介绍 Nix 中如何将名称**分配给值**,以及最常用的数据类型——**属性集**,继而引出**递归属性集**与**列表**的概念。 +变量是大多数编程语言中最基础的概念, +而与之类似的名称则是 Nix 语言中最基础的概念。 +本节将会介绍 Nix 中如何将名称**分配给值**, +以及最常用的数据类型——**属性集**, +继而引出**递归属性集**与**列表**的概念。 ### 名称和值 -我们可以使用 `=` 将名称分配给值,形成“名称 - 值”对。例如将名称 `foo` 分配给值 `123`: +我们可以使用 `=` 将名称(name)分配给值(value),形成“名称—值”对。 + +例如将名称 `foo` 分配给值 `123`: ```nix foo = 123 ``` -::: info -上面的示例不属于表达式(但可以作为表达式的一部分),所以你无法将它直接写入 nix 文件进行文件求值;不过 `nix repl` 有一些灵活的处理,允许你输入这样的结构。 +::: tip 如何测试此示例 +上面的示例不属于表达式(但可以作为表达式的一部分), +所以你无法将它直接写入 nix 文件进行文件求值; +不过 `nix repl` 有一些灵活的处理,允许你输入这样的结构。 ::: ::: info 函数式语言与命令式语言中“变量”的区别 -太长不看版:在 Nix 里,“名称”就是“变量”,只是这个变量一旦绑定便成永恒;它保留了数学“可取不同值”的语义,却丢掉了命令式“可重新赋值”的含义。 +太长不看版:在 Nix 里,“名称”就是“变量”, +只是这个变量一旦绑定便成永恒; +它保留了数学“可取不同值”的语义, +却丢掉了命令式“可重新赋值”的含义。 | 维度 | 命令式语言 | Nix(函数式) | |---|---|---| -| 底层模型 | 存储格(内存单元) | 无存储格,只有「名称-值」映射 | +| 底层模型 | 存储格(内存单元) | 无存储格,只有「名称—值」映射 | | 操作 | 赋值:随时把新值写回同一单元 | 绑定:一次性把名称贴到值,不可重写 | | 所谓“变量” | 存储格的别名 → 之后可反复擦写 | 数学意义上的变量 → 同一作用域内值固定 | | 文档用词 | variable = 可重写的存储格 | variable = 一次性绑定的名称(不会变) | @@ -221,8 +232,8 @@ foo = 123 ::: -名称的值并不仅限于 `123` 这种整数。具体来说有以下数据类型(不需要完全理解,留下印象 -即可) +名称的值并不仅限于 `123` 这种整数。 +一些常见的数据类型如下(不需要完全理解,留下印象即可) - 字符串(string),例如 `"Hello world"` - 整数(integer),例如 `1` @@ -231,6 +242,7 @@ foo = 123 - null,只有 `null` 一种 - 列表(list),例如 `[ 1 "tux" false ]` - 属性集(attribute set),例如 `{ a = 1; b = "tux"; c = false; }` +- 函数(function),例如 `x: x + 1` ### 属性集 @@ -251,17 +263,15 @@ foo = 123 语法说明: -- 属性集以 `{` `}` 为边界,其内部为多个“名称-值”对,且它们末尾必须添加 `;` 。 - -上述代码将 `foo` 的值定义为属性集 `{ a = 1; b = 2; }` ,因此可称之为属性集 `foo` 。 - -属性集 `foo` 中有两个属性: +- 属性集以 `{` `}` 为边界,其内部为多个“名称—值”对,且它们末尾必须添加 `;` 。 +属性集 `{ a = 1; b = 2; }` 中有两个属性: - 属性 `a`,其值为 `1` - 属性 `b`,其值为 `2` -属性的值除了可以是 `1` `2` 这样的数值外,也可以是一个属性集(也即支持嵌套),例如 -将 `b` 的值改为属性集 `{ c = 2; d = 3; }`: +属性的值除了可以是 `1` `2` 这样的数值外, +也可以是一个属性集(也即支持嵌套), +例如将 `b` 的值改为属性集 `{ c = 2; d = 3; }`: ```nix { @@ -273,7 +283,8 @@ foo = 123 } ``` -嵌套属性集中的属性也可以利用 `.` 表示,例如上面这段的一种等价写法如下: +嵌套属性集中的属性也可以利用 `.` 表示, +例如上面这段的一种等价写法如下: ```nix { @@ -303,7 +314,11 @@ foo = 123 ``` error: undefined variable 'a' ``` -可见,当属性集内的属性 `b` 需要访问该属性集的另一个属性 `a` 时,即使 `a` 是“先”定义的,也无法访问到。此时就需要我们改用递归(recursive)属性集,它相比普通的属性集,在前面多加了 `rec `: +可见,当属性集内的属性 `b` 需要访问该属性集的另一个属性 `a` 时, +即使 `a` 是“先”定义的,也无法访问到。 + +此时就需要我们改用递归(recursive)属性集, +它相比普通的属性集,在前面多加了 `rec `: ```nix rec { @@ -319,14 +334,22 @@ rec { ``` -::: note 求值结果的排序依据 -可以看到,结果中的 `a = 1` 在前面,`b = 3` 在后面。这种顺序实际上与任何其它因素(包括声明顺序、求值依赖关系)都无关,而只与**属性名称本身的排序**有关。例如,对 `rec { a = 1; b = 2; }` 与 `rec { b = 2; a = 1; }` 的求值,都会把 `a = 1` 放在前面,归因到底,只是 `a` 在字母表中位于 `b` 之前罢了。(直接原因则与 Nix 解释器对名称排序所用到的算法或者调用的库有关,这里不再深入。) +::: info 求值结果的排序依据 +可以看到,结果中的 `a = 1` 在前面,`b = 3` 在后面。 +这种顺序实际上与任何其它因素(包括声明顺序、求值依赖关系)都无关, +而只与**属性名称本身的排序**有关。 + +例如,对 `rec { a = 1; b = 2; }` 与 `rec { b = 2; a = 1; }` 的求值, +都会把 `a = 1` 放在前面,归因到底, +只是 `a` 在字母表中位于 `b` 之前罢了。 + +_(直接原因则与 Nix 解释器对名称排序所用到的算法或者调用的库有关,这里不再深入。)_ ::: -::: info 求值过程的顺序机制 +::: note 拓展说明:求值过程的顺序机制 既然求值结果的排序与求值顺序等因素无关,那么求值顺序由什么决定呢? 将刚才例子中属性集里的两个元素位置对调: @@ -336,7 +359,12 @@ rec { a = 1; } ``` -你会发现,Nix 解释器能自动处理求值顺序,并不会因为 `a` 的声明被调整到后面而影响求值结果(与之前的完全一致,从略)。这看起来相当“智能”,你甚至可以写得更复杂一些,比如 Nix 解释器也能自动处理下面的例子(结果略): +你会发现,Nix 解释器似乎能自动处理求值顺序, +并不会因为 `a` 的声明被调整到后面而影响求值结果 +(与之前的完全一致,从略)。 + +这看起来相当“智能”,你甚至可以写得更复杂一些, +比如 Nix 解释器也能自动处理下面的例子(结果略): ```nix rec { c = a * 2 - b + d - 35; @@ -345,8 +373,8 @@ rec { d = a - 15; } ``` - -不过,这并不代表你可以直接用它来解方程。例如我们再写一个在数学上有唯一解的方程组: +不过,这并不代表你可以直接用它来解方程。 +例如我们再写一个在数学上有唯一解的方程组: ```nix rec { b = a * 2 + 1; @@ -360,7 +388,8 @@ rec { b = «error: infinite recursion encountered»; } ``` -由此可见,递归属性集内部处理求值顺序的机制,确实是递归的,而如果递归陷入死循环就会报错。 +由此可见,递归属性集内部处理求值顺序的机制, +确实是递归的,而如果递归陷入死循环就会报错。 ::: @@ -374,18 +403,24 @@ rec { c = "banana"; } ``` -上面的名称 `a` `b` `c` 或许可以有明确的含义,但有些场景不需要这些名称,而只关心后面的值,这种情况下就可以使用列表,例如: +上面的名称 `a` `b` `c` 或许可以有明确的含义, +但有些场景不需要这些名称,而只关心后面的值, +这种情况下就可以使用列表,例如: ```nix [ "apple" "orange" "banana" ] ``` 需要注意语法细节: -- 列表以 `[` `]` 为边界,其内部为多个元素,每个元素都是值(value)。 +- 列表以 `[` `]` 为边界,其内部为多个元素,每个元素都是值。 - 元素之间使用空格(或换行)分隔,各元素**不**以 `;` 结尾。 ## let 绑定与属性访问 -前面关于名称的使用是非常基本的,我们还需要更灵活的处理方法。本节将会介绍另一种将名称分配给值的方法——**let 绑定**,以及风格简洁的**属性访问**。 +前面关于名称的使用是非常基本的, +本节要介绍的**let 绑定**和**属性访问** +则提供了更灵活的处理方法。 + ### `let` 绑定 -有时我们希望在指定的范围内为值分配名称,此时就可以使用 `let` 绑定,示例如下: +有时我们希望在指定的范围内为值分配名称, +此时就可以使用 `let` 绑定,示例如下: ```nix let @@ -396,7 +431,7 @@ in ``` 注意语法细节: -- `let` 与 `in` 之间的“名称-值”对以 `;` 结尾; +- `let` 与 `in` 之间的“名称—值”对以 `;` 结尾; - `in` 之后**只有一个表达式**。注意,这只是语法形式上的要求,并不代表 `let` 绑定的用处很有限,因为表达式本身可以很复杂,常见的是嵌套属性集。作为基本示例,下面演示刚刚学到的列表: ```nix let @@ -413,17 +448,17 @@ in ::: info 作用域 -`let` 绑定是有作用域的,绑定的名称只能在作用域使用,或者说每个 `let` 绑定的名称只能在该表达式内使用。例如下面的例子: +`let` 绑定是有作用域的,绑定的名称只能在作用域使用, +或者说每个 `let` 绑定的名称只能在该表达式内使用。 +例如下面的例子: ```nix { a = let x = 1; in x; b = x; } ``` - 由于 `b = x;` 不在作用域之内,会有报错如下: - ``` error: undefined variable 'x' ``` @@ -432,22 +467,27 @@ error: undefined variable 'x' ::: -::: note 局部变量(?) -Nix 中不存在“全局变量”,因而“局部变量”的说法可能引起误会,应当尽量避免使用。 +::: note 拓展说明:局部变量(?) +Nix 中不存在“全局变量”, +因而“局部变量”的说法可能引起误会, +应当尽量避免使用。 不过,[Nix manual](https://nix.dev/manual/nix/2.31/language/syntax.html) 中对 let 绑定的介绍提到了局部变量(local variable)。 > A let-expression allows you to define local variables for an expression. -这种说法可能不合适,但既然官方文档也有用到,其他地方自然也可能会出现,留心即可。 +这种说法可能不合适,但既然官方文档也有用到, +其他地方自然也可能会出现,留心即可。 ::: ### 属性访问 -前面提到,嵌套属性集中的属性可以利用 `.` 表示,这被称为属性访问(attribute access)。 +前面提到,嵌套属性集中的属性可以利用 `.` 表示, +这被称为属性访问(attribute access)。 -在下面这个例子中,我们定义了一个嵌套属性集 `a`,并使用 `a.b.c` 访问值 `123`: +在下面这个例子中,我们定义了一个嵌套属性集 `a`, +并使用 `a.b.c` 访问值 `123`: ```nix let a = { b = { c = 123; }; }; @@ -475,9 +515,11 @@ in ::: tip 小结 -本小节给出了属性访问的两种应用场景,第一种是获取属性的值,第二种是为值分配属性名称。 +本小节给出了属性访问的两种应用场景, +第一种是获取属性的值,第二种是为值分配属性名称。 -显然,第二种场景**不是**必须使用属性访问的写法,它只是更方便。仅就这个场景来看,这是一种**语法糖**。 +显然,第二种场景**不是**必须使用属性访问的写法,它只是更方便。 +仅就这个场景来看,这是一种**语法糖**。 我们将在下一节介绍另外两种常用的语法糖。 @@ -485,7 +527,10 @@ in ::: ## 语法糖 `with` 和 `inherit` -语法糖(syntactic sugar)是对语言功能没有影响,但更方便使用的一种语法。本节将介绍两种常用的语法糖 `with` 和 `inherit`。 +语法糖(syntactic sugar)是对语言功能没有影响, +但更方便使用的一种语法。 + +本节将介绍两种常用的语法糖 `with` 和 `inherit`。 ### `with` 表达式 @@ -514,7 +559,7 @@ in ``` -::: note 就近性 +::: info 就近性 不过,这种等价并不是恒定的, 比如下面的例子, 我们在 `let` 后面直接加一行 `x = 0;`: @@ -571,7 +616,7 @@ in - 而 `x` `y` 则直接从同名变量获取值,这被称为“继承”(inherit)。 下面将要介绍的 `inherit` 语法, -则简化了这种继承所需的“名称-值”对。 +则简化了这种继承所需的“名称—值”对。 比如,刚才的例子可以这样写(求值结果不变): ```nix let @@ -592,7 +637,8 @@ in ::: warning -`inherit` 的语法结构,例如上面的 `inherit x;`,本质上仍然属于“名称-值”对,不属于表达式。 +`inherit` 的语法结构,例如上面的 `inherit x;`, +本质上仍然属于“名称—值”对,不属于表达式。 ::: @@ -651,10 +697,15 @@ in ## 文件系统路径 -在 Nix 语言中,文件系统路径(file system paths;简称路径)是一种数据类型,它不同于后面要介绍的字符串类型。 +在 Nix 语言中,文件系统路径(file system paths;简称路径)是一种数据类型, +它不同于后面要介绍的字符串类型。 ### 路径的基本语法 +在 Nix 语言中, +路径的基本语法与 POSIX 的路径虽有共通之处, +但有细节上的差异,不注意的话很容易导致问题。 + 路径有绝对路径(absolute path) 和相对路径(relative path)两种, 它们都必须满足: @@ -790,7 +841,8 @@ error: syntax error, unexpected '.' ## 字符串 -字符串(string)是一种常见的数据类型,其最简单的形式是以一对双引号 `"` `"` 包裹所需内容。 +字符串(string)是一种常见的数据类型, +其最简单的形式是以一对双引号 `"` `"` 包裹所需内容。 例如: @@ -820,7 +872,7 @@ in ::: warning 名称的值的数据类型 字符串插值语法支持的值必须为字符串类型, -或是可以转换为字符串的数据类型。例如: +例如: ```nix let @@ -940,7 +992,7 @@ to get distro info. ``` -::: note 智能去除缩进 +::: info 智能去除缩进 Nix 的多行字符串会统一去除开头的缩进, 这在其他语言中是不常见的。 @@ -997,16 +1049,14 @@ in '' ``` -但如果我们希望在字符串中使用原始字符 `''`, -因为会与多行字符串原有的语义冲突, -不能直接写 `''`,而必须改用 `'''` 三个单引号。 - -也就是说, -在多行字符串中的 `'''` 三个单引号这样的组合, -实际输出的是原始字符串 `''`. + +::: note 拓展说明:连续多个单引号 -举个例子: +对于多个单引号来说,因为 `''` 本身被用来转义,输出它们的方法有些特殊: +- 若要在字符串中使用原始字符 `''`(2 个),可以用 `'''`(3 个)。 +- 若要在字符串中使用原始字符 `'''`(3 个),可以用 `''''`(4 个)。 +例如: ```nix let a = "1"; @@ -1020,268 +1070,675 @@ in "the value of a is:\n ''1''\n" ``` +而对于更多的单引号,转义机制较为复杂。 +- 一般地,当存在连续 $n$ 个单引号时($n\ge 3,n\in \mathbb Z$),令 $N=n\bmod 3$, + - 若 $N\neq 2$ 则会转义出 $\frac{n-N}{3}\times 2+N=\frac{2n+N}{3}$ 个单引号。 + - 若 $N=2$ 则会报错: + ```plain + error: syntax error, unexpected end of file, expecting IND_STR or DOLLAR_CURLY or IND_STRING_CLOSE + ``` +- 反过来说,若需要在字符串中使用连续 $m$ 个单引号作为原始字符($m\ge 2,m\in \mathbb Z$),令 $M=m\bmod 2$,则需要 $M+\frac{m-M}{2}\times 3=\frac{3m-M}{2}$ 个单引号来进行转义。 + + +::: + ## 函数 -函数在 Nix 语言中是人上人,我们先来声明一个匿名函数(Lambda): +作为一门函数式编程语言(functional programming language), +Nix 中函数的地位非常重要。 + +### 函数的基本构成 +函数由参数和函数体组合而成,它们之间由 `:` 分隔。 + +例如,对于数学上的函数 $f(x)=x+1$ ,用 Nix 的函数表达如下: ```nix x: x + 1 ``` -冒号左边是函数参数,冒号右边跟随一个空格,随即是函数体。 +在此例中,冒号左边的 `x` 是参数,右边的 `x+1` 是函数体。 -这是个嵌套的函数,支持多重参数(柯里化函数): + +::: info 匿名函数与 λ +机智的你可能会发现, +此示例实现的 $f(x)=x+1$ 并不完整—— -```nix -x: y: x + y +毕竟,$f(x)=x+1$ 的函数名 $f$ 去哪里了? + +确实,上面的例子少了函数名, +它没有和名称绑定,被称为**匿名函数**。 + +我们对它进行求值,结果如下: +```plain + ``` -参数当然可以是属性集类型: +这里的 LAMBDA(即希腊字母 λ)就是函数的代表符号。 + +在一些语言中,λ 特指匿名函数, +不过,在 Nix 语言中,`` 只是一种数据类型,指代一般的函数。 + +至于为什么 λ 被用来代表函数, +请自行搜索“lambda 演算”以及“函数式编程”, +这里不再展开。 + + +::: + +::: tip 直接调用匿名函数 +利用 `(` `)` 将匿名函数的整体包裹起来,就可以直接调用了,例如 ```nix -{ a, b }: a + b +(x: x + 1) 2 ``` +求值结果为 `3`。 -为函数指定默认参数,在缺少该参数的值的情况下,它就是默认值: + +::: + +函数是一种数据类型,自然可以将函数与名称绑定。 + +沿续前一个例子,我们将函数 `x: x + 1` 绑定到名称 `f`,并且将 2 作为其参数来调用: ```nix -{ a, b ? 0 }: a + b +let + f = x: x + 1; +in + f 2 +``` + +求值,结果如下: +```plain +3 ``` +这相当于先定义函数 $f(x)=x+1$ ,再求 $f(2)$ 的值,结果为 3。 -允许传入额外的属性: +### 作为参数的属性集:基本形式 +在前面的例子中,我们只实现了一个简单的一元函数 $f(x)=x+1$。 + +那么对于多元函数,比如 $f(x,y)=3x+\frac{y}{2}$,在 Nix 中应该怎么实现呢? +- 坏消息是,根据 Nix 语法规范,每个函数在形式上**有且仅有一个参数**。 +- 好消息是,这个参数**可以是属性集**,并且在函数体中可以将属性集中的**各个属性单独拿出来使用**。 + + +::: tip +“每个函数在形式上有且仅有一个参数”, +这个特性其实不算缺点, +比如它为函数的柯里化(之后会介绍)提供了方便。 + + +::: + +例如 ```nix -{ a, b, ...}: a + b # 明确传入的属性有 a 和 b,传入额外的属性将被忽略 -{ a, b, ...}: a + b + c # 即使传入的属性有 c,一样不会参与计算,这里会报错 +{ x, y }: ( 3 * x ) + ( y / 2 ) ``` -为额外的参数绑定到参数集,然后调用: +上面的函数虽然仅接受一个参数(属性集 `{ x, y }`), +实际功能却相当于数学上的二元函数 $f(x,y)=3x+\frac{y}{2}$。 + + +::: warning 属性集的语法细节 +在**函数定义**中作为参数出现的属性集,只包含属性名称,并且用 `,` 分隔。 + +这与之前介绍的属性集和列表都不同。 +作为对比,下面是一个标准的属性集的示例: ```nix -args@{ a, b, ... }: a + b + args.c -{ a, b, ... }@args: a + b + args.c # 也可以是这样 +# 注意分号不是分隔而是后缀,这里出现了两次 +{ a = 1; b = 2; } +``` + +再来一个列表的示例: +```nix +# 用空格分隔 +[ a b ] ``` -为函数命名: + +::: + +我们为前面例子中的匿名函数绑定名称 `f`, +并且以参数 `{ x = 1; y = 4; }` 来调用它: ```nix let - f = x: x + 1; + f = { x, y }: ( 3 * x ) + ( y / 2 ); in - f + f { x = 1; y = 4; } ``` +求值,结果如下: +```plain +5 +``` +这相当于定义了函数 $f(x,y)=3x+\frac{y}{2}$ 之后求值 $f(1,4)$,结果为 5。 -调用函数,并使用函数构建新属性集: + +::: tip 更多示例 +Nix 的函数也能处理其它数据类型。 +例如,定义一个函数 `concat3` 并调用它来拼接 `"Hello"` `" "` 和 `"world"`: ```nix -concat = { a, b }: a + b # 等价于 concat = x: x.a + x.b -concat { a = "Hello "; b = "NixOS"; } +let + concat3 = { a, b, c }: a + b + c; +in + concat3 { a = "Hello"; b = " "; c = "world"; } +``` +求值结果如下: +```plain +"Hello world" ``` -输出: - +调用函数进行的求值,自然也可以嵌套使用。 +例如,定义一个函数 `concat2`, +并两次调用它来拼接 `"Hello"` `" "` 和 `"world"`: ```nix -Hello NixOS +let + concat2 = { a, b }: a + b; +in + concat2 { + a = concat2 { a = "Hello"; b = " "; }; + b = "world"; + } ``` +由于 `concat2` 接受的属性集仅含两个属性, +此例先拼接了 `"Hello"` 和 `" "`,再将此结果与 `"world"` 拼接。 +求值结果仍然为 `"Hello world"`。 -由于函数与参数使用空格分隔,所以我们可以使用括号将函数体与参数分开: + +::: + +::: warning +函数被调用时所接受的属性集, +必须符合**定义中作为参数的属性集的要求**, +否则就会报错。 + +例如前面的 `concat2` 函数,我们多给一个 `c` 的值: ```nix -(x: x + 1) 1 # 向该 Lambda 函数传入参数 1 +let + concat2 = { a, b }: a + b; +in + concat2 { a = "Hello"; b = "world"; c = "!"; } +``` +求值,报错: +```plain +error: function 'concat2' called with unexpected argument 'c' +``` +或者,这次我们只给出 `b` 的值: +```nix +let + concat2 = { a, b }: a + b; +in + concat2 { b = "world"; } +``` +求值,报错: +```plain +error: function 'concat2' called without required argument 'a' ``` -## 柯里化函数 +但是,前述要求可以设置得更加灵活, +下面的若干节将会对此进行介绍。 -我们将 `f (a,b,c)` 转换为 `f (a)(b)(c)` 的过程就是柯里化。为什么需要柯里化?因为 -它很灵活,可以避免重复传入参数,当你传入第一个参数的时候,该函数就已经具有了第一 -个参数的状态(闭包)。 + +::: -尝试声明一个柯里化函数: +### 作为参数的属性集:属性默认值 +在属性后面加 `? ` ,会将此属性的默认值设为 ``。 -```nix -x: y: x + y -``` +在下面的例子中,我们来定义一个“问候”函数 `greet`。其功能是: +- 使用作为问候语的参数 `greeting`, +- 对作为问候对象的参数 `object` 进行“问候”。 -为了更好的可读性,我们推荐你这样写: +我们可以将最常用的问候语(例如 `"Hello, "`)作为默认值, +这样就可以选择不传入此参数,而直接采用默认值。 +实例如下,注意在函数定义中,我们在参数 `greeting` 后面附加了 `? "Hello, "`: ```nix -x: (y: x + y) +let + greet = { greeting ? "Hello, ", object }: greeting + object + "!"; +in + { + # 对 world 进行问候(默认问候语) + R1 = greet { object = "world"; } ; + # 对 my friend 进行问候(默认问候语) + R2 = greet { object = "my friend"; } ; + # 对 my friend 进行问候(自定义问候语) + R3 = greet { greeting = "Welcome, "; object = "my friend"; } ; + } +``` +严格求值,结果如下: +```plain +{ + R1 = "Hello, world!"; + R2 = "Hello, my friend!"; + R3 = "Welcome, my friend!"; +} ``` -这个例子中的柯里化函数,虽然接收两个参数,但不是"迫切"需要: +### 作为参数的属性集:额外属性 +前面已经提到,在调用函数时如果传入额外属性,会引发报错。 +但有时我们需要传入额外属性,此时就需要在属性集中添加一个占位符 `...`。 +例如: ```nix let - f = x: y: x + y; + concat2 = { a, b, ... }: a + b; in - f 1 +{ + R1 = concat2 { a = "Hello "; b = "world"; }; + # 传入额外属性 c,这次不会引发报错 + R2 = concat2 { a = "Hello "; b = "world"; c = "!"; }; +} ``` +严格求值结果如下: +```plain +{ + R1 = "Hello world"; + R2 = "Hello world"; +} +``` +注意这里的 `R1` 和 `R2` 的值相同, +因为 `c` 作为额外属性,不能出现在函数定义中,自然也不会参与计算。 -输出为: - + +::: warning +在函数定义中,若函数体使用了参数中未定义的属性,不论参数是否含 `...` 都会报错。 +例如: ```nix - +let + # 参数中没有 c,但函数体里有 c + concat2 = { a, b, ... }: a + b + c; +in + concat2 { a = "Hello "; b = "world"; c = "!"; } +``` +求值,报错(注意这个报错发生在函数的定义部分): +```plain +error: undefined variable 'c' ``` -`f 1` 的值依然是函数,这个函数大概是: + +::: + +### 作为参数的属性集:命名属性集 +这里再次展示前面举过的例子, +定义函数 $f(x,y)=3x+\frac{y}{2}$,求值 $f(1,4)=5$, +用 Nix 实现如下: ```nix -y: 1 + y; +let + f = { x, y }: ( 3 * x ) + ( y / 2 ); +in + f { x = 1; y = 4; } +``` +求值,结果如下: +```plain +5 ``` -我们可以保存这个状态的函数,稍后再来使用: + +::: info 命名属性集 +与匿名函数的概念类似,若一个属性集没有与名称绑定, +则称其为匿名属性集。反之,则称为命名属性集。 + +::: -```nix +此例的函数定义中,匿名属性集 `{ x, y }` 作为了参数。 + +而命名属性集也可以作为参数,此时往往需要结合属性访问。 + +例如,上面的例子等价于:(求值结果不变) +``` let - f = x: y: x + y; + # 用命名属性集 A 代替了匿名属性集 { x, y } + # 同时 x、y 也要改用属性访问的写法 A.x、A.y + f = A: ( 3 * A.x ) + ( A.y / 2 ); in - let g = f 1; in g 2 + f { x = 1; y = 4; } ``` -也可以一次性接收两个参数: +此外,函数的参数可以是**一个**命名属性集与**一个**匿名属性集的结合, +两者以 `@` 连接(先后顺序不限),并且匿名属性集必须包含 `...` 以允许额外属性。 +例如,上面的例子还等价于:(求值结果不变) ```nix let - f = x: y: x + y; + f = { x, ... }@A: ( 3 * x ) + ( A.y / 2 ); + # 也可以写成 + # f = A@{ x, ... }: ( 3 * x ) + ( A.y / 2 ); in - f 1 2 + f { x = 1; y = 4; } ``` -## 属性集参数 - -当我们被要求必须传入多个参数时,使用这种函数声明方法: +### 柯里化函数 +前面已经提到如何直接调用匿名函数, +现在考虑下面的表达式: +```nix +( y : 1 * y ) 10 +# 结果为 10 +``` +再考虑表达式: ```nix -{a, b}: a + b +( y : 2 * y ) 10 +# 结果为 20 ``` +再考虑表达式: +```nix +( y : 3 * y ) 10 +# 结果为 30 +``` +可以看到,尽管参数始终为 10,表达式的结果会随着匿名函数的函数体内部这个乘数的变化(从 1、2 变到 3)而变化(从 10、20 变到 30)。本质上,这是**函数**本身在随着这个**乘数**的变化而变化。 -调用该函数: +这种变化关系自然也是一种函数关系。 +换句话说,设这个乘数为 `x`, +这形成了由 `x` 到 ` y : x * y` 的函数关系。 +这个函数关系本身,也可以用 Nix 的函数来实现: +```nix +x : ( y : x * y ) +# 虽然会降低可读性,也可以这样写: +# x : y : x * y +``` +这个函数接受一个参数, +我们将其命名为 `f` 并传入 4 来测试: ```nix let - f = {a, b}: a + b; + f = x : ( y : x * y ); in - f { a = 1; b = 2; } + f 4 +``` +求值,结果如下: +```plain + ``` +不出我们预料,`f 4` 是一个函数。 +实际上这个函数正是 +```nix +y : 4 * y +``` +`f 4` 接受一个参数,我们传入 10 来测试: +```nix +let + f = x : ( y : x * y ); +in + f 4 10 +``` +求值结果为 40。 -如果我们额外传入参数,会怎么样? +以上,通过两个一元函数的嵌套, +我们得以先传入一个参数,再传入另一个参数。 +就最终的实际功能来说, +这个函数与普通的二元函数都能接受两个参数; +只不过,之前我们实现二元函数的方法是利用属性集, +比如 ```nix let - f = {a, b}: a + b; + g = { x, y }: x * y; in - f { a = 1; b = 2; c = 3; } + g { x = 4; y = 10; } ``` +求值结果仍然为 40。 -意外参数 `c`: +观察前面例子中 `g` 和 `f` 的函数定义, +它们的函数体中都含有 `x * y`, +只是接受参数的方式不同。 -```bash -error: 'f' at (string):2:7 called with unexpected argument 'c' +由 `g` 到 `f`, +相当于把**一个二元函数** `g` +改写为了函数 `f` 这样的**两个一元函数的嵌套序列**。 - at «string»:4:1: +**一般地,将一个 $n$ 元函数改写为 $n$ 个一元函数的嵌套序列,这个过程就被称为柯里化(currying)。** - 3| in - 4| f { a = 1; b = 2; c = 3; } - | ^ - 5| -``` + +::: tip 柯里的由来 +“柯里”是 curry 的音译(也可译作“卡瑞”“加里”等), +它得名自数理逻辑学家 Haskell Brooks Curry。 + +curry 还有其它音译, +但它们可能代表完全不同的其他含义, +例如咖喱、库里,等等。 + + +::: + + +::: info 柯里化:闭包与嵌套 -## 默认参数 +在前面的例子中, +函数 `f 4` 保存了 `x = 4` 的这种状态, +这种函数被称为闭包(closure)。 -前面稍微提到过一点,没有什么需要过多讲解的地方: +_(支持闭包机制的语言很多, +并且尤其在 Javascript 等采用动态变量的语言中, +闭包的一个重要作用就是将捕捉闭包时外部变量的状态保存下来; +但本文的例子中,闭包则是保存传入参数的值,不涉及外部变量。 +受篇幅限制且为了避免理解困难,这里不介绍闭包的完整概念, +感兴趣可自行了解。)_ +实际上,柯里化就是通过闭包与嵌套(或者说递归)来实现的。 + +闭包,有时可以避免重复传入参数。 + +之前,为了演示默认值,我们自定义了一个问候函数 `greet`: ```nix let - f = {a, b ? 0}: a + b; + greet = { greeting ? "Hello, ", object }: greeting + object + "!"; in - f { a = 1; } + { + R1 = greet { object = "world"; } ; + R2 = greet { object = "my friend"; } ; + R3 = greet { greeting = "Welcome, "; object = "my friend"; } ; + } ``` - -传入参数值是可选的,根据你的需要来: - +这里我们将 `greet` 函数柯里化,利用闭包来实现, +甚至可以更加简洁(求值结果与之前例子相同): ```nix let - f = {a, b ? 0}: a + b; + greet = greeting : ( object : greeting + object + "!" ); + # greet_Hello 就是一个闭包,调用它可以避免重复传入 "Hello, " + greet_Hello = greet "Hello, "; in - f { a = 1; b = 2; } + { + # 对 world 进行问候(用 greet_Hello) + R1 = greet_Hello "world"; + # 对 my friend 进行问候(用 greet_Hello) + R2 = greet_Hello "my friend"; + # 对 my friend 进行问候(自定义问候语) + R3 = greet "Welcome, " "my friend"; + } ``` -## 额外参数 + +::: -有的时候,我们设计的函数不得不接收一些我们不需要的额外参数,我们可以使用 `...` -允许接收额外参数: + +::: note 拓展说明:数学上的柯里化 -```nix -{a, b, ...}: a + b -``` +柯里化中的“一元函数”并不是数学意义上的函数,在数学上对应的概念实际上是映射。 -不比上个例子,这次不会报错: +例如,三元函数 $F(x,y,z)=x+y+z$ 也即映射 $F: x,y,z\mapsto x+y+z$ +可以转换为三个一元映射的嵌套,分别是: +1. $F_1: x \mapsto ( y \mapsto ( z \mapsto x + y + z ))$ +2. $F_2: y \mapsto ( z \mapsto x + y + z )$ +3. $F_3: z \mapsto x + y + z$ + + +::: + +## 函数库 + +前面我们已经接触到了 `+`、`-`、`*`、`/` 等运算符号, +实际上它们都属于 Nix 语言中的内建操作符(built-in operator)。 + +常用的内建操作符还有 `==` `&&` 等。 +建议至少浏览一遍[内建操作符的文档页面](https://nix.dev/manual/nix/stable/language/operators.html), +以熟悉可用的功能。 + +除了内建操作符之外,还有两个被广泛使用的函数库(function library), +它们加在一起被视为 Nix 语言的事实标准。 + +### builtins +builtins 即内建函数,也称为“原始操作” +(primitive operations,简写为 primops)。 + +Nix 附带许多内建函数, +均在 [Nix 手册](https://nix.dev/manual/nix/stable/language/builtins.html) 列出。 + +这些函数可以通过常量 `builtins` 访问,例如前面提到过的 `toString`: ```nix -let - f = {a, b, ...}: a + b; -in - f { a = 1; b = 2; c = 3; } +builtins.toString ``` +求值,结果如下: +```plain + +``` + + +::: note 拓展说明:primop 类型 +注意这里返回的结果不是 ``, +说明内建函数与普通的函数是有差别的。 + +实际上,普通的函数由 Nix 语言实现, +而这些内建函数则作为 Nix 语言解释器的一部分,由 C++ 实现。 + + +::: -## 命名参数集 + +::: info import 函数 +有些内置函数只能通过 `builtins` 访问, +但另有一些内置函数,可直接在顶层使用,比如 `import`, `toString`, `map`。 -又名 "`@` 模式"。在上文中,我们已经可以接收到额外的参数了,假如我们需要使用某 -个额外参数,我们可以使用命名属性集将其接收到一个另外的属性集: +`import` 接受的参数是 Nix 文件的路径, +会对其进行文件求值并返回结果。 +此路径也可以是目录, +这种情况下则会使用该目录下的 `default.nix` 文件。 +例如,令 `foo.nix` 的文件内容为 `1 + 2`, +有如下示例 ```nix -{a, b, ...}@args: a + b + args.c # 这样声明函数 -args@{a, b, ...}: a + b + args.c # 或是这样 +import ./foo.nix ``` +求值,结果为 `3`。 -具体示例如下: +被导入的 Nix 文件必须是 Nix 表达式, +这个表达式自然也可以是函数本身, +而函数是可以接受参数的。 +例如,令 `foo.nix` 的文件内容为 `x: x + 1`, +有如下示例 ```nix -let - f = {a, b, ...}@args: a + b + args.c; -in - f { a = 1; b = 2; c = 3; } +import ./foo.nix 4 ``` +求值,结果为 `5`。 -## 函数库 + +::: + +### pkgs.lib -除了一些 -[内建操作符](https://nixos.org/manual/nix/stable/language/operators.html) (`+`, -`==`, `&&`, 等),我们还要学习一些被视为事实标准的库。 +[`nixpkgs`](https://github.com/NixOS/nixpkgs) 仓库包含一个名为 [`lib`](https://github.com/NixOS/nixpkgs/blob/master/lib/default.nix) 的属性集, +它提供了大量有用的函数,详见 [Nixpkgs 手册](https://nixos.org/manual/nixpkgs/stable/#sec-functions-library)。 -### 内建函数 +这些函数是基于 Nix 语言实现的, +而不是像 `builtins` 那样本身作为语言的一部分而存在。 -它们在 Nix 语言中并不是 `` 类型,而是 `` 元操作类型(primitive -operations)。这些函数是内置在 Nix 解释器中,由 C++ 实现。查询 -[内建函数](https://nixos.org/manual/nix/stable/language/builtins.html) 以了解其 -使用方法。 +由于 Nixpkgs 的属性集通常约定命名为 `pkgs`, +因此往往可以通过 `pkgs.lib` 使用这些函数。 + +_(其实,当下直接使用 `lib` 而不是 `pkgs.lib` 的情况更常见,后面会提到。)_ + +例如能够将小写转大写的 `pkgs.lib.strings.toUpper` 函数,示例: ```nix -builtins.toString() # 通过 builtins 使用函数 +let + pkgs = import {}; +in +pkgs.lib.strings.toUpper "Have a good day!" +``` +求值,结果如下: +```plain +"HAVE A GOOD DAY!" ``` -### 导入 + +::: tip 详细说明 +上面的例子较为复杂,不过到现在你应该熟悉它的各个组成部分了。 -`import` 表达式以其他 Nix 文件的路径为参数,返回该 Nix 文件的求值结果。 +名称 `pkgs` 被声明为从路径为 `` 的文件 `import` 出来的表达式。 +至于 `` 的具体值则由环境变量 `$NIX_PATH` 决定。 +由于该表达式是一个函数,需要一个参数才能求值, +在这个例子中传入空的属性集 `{}` 就足够了。 -`import` 的参数如果为文件夹路径,那么会返回该文件夹下的 `default.nix` 文件的执行 -结果。 +现在 `pkgs` 在 `let ... in ...` 的作用域内,其下的属性可以被访问。 +据 Nixpkgs 手册可知, +其下存在一个函数 [`lib.strings.toUpper`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.strings.toUpper), +作用是小写转大写: +> Converts an ASCII string s to upper-case. -如下示例中,`import` 会导入 `./file.nix` 文件,并返回该文件的求值结果: + +::: + +::: info Nix 生态中 pkgs、pkgs.lib 和 lib 的约定俗成 + +`pkgs` 常被作为参数传递给函数。 +按约定,可以假设它指的是 Nixpkgs 的属性集, +该属性集有一个 `lib` 属性。 + +例如,将下面的例子写入 `foo.nix`: +```nix +{ pkgs, ... }: +pkgs.lib.strings.removePrefix "I " "I see you!" +``` +在命令行将 `{ pkgs = import {}; }` 作为参数, +进行文件求值: ```bash -$ echo 1 + 2 > file.nix -import ./file.nix -3 +nix-instantiate --eval foo.nix --arg pkgs 'import {}' +``` +运行结果如下: +```plain +"see you!" ``` -被导入的 Nix 文件可以返回任何内容,返回值可以向上面的例子一样是数值,也可以是属 -性集(attribute set)、函数、列表,等等。 - -如下示例导入了 `file.nix` 文件中定义的一个函数,并使用参数调用了该函数: +而在 NixOS 配置中以及 Nixpkgs 内部, +你还经常会看到直接传入 `lib` 的情况。 +此时可以假设它指的是 Nixpkgs 的属性集下的 `lib`, +也即前面那种情况下的 `pkgs.lib`。 +例如,将下面的例子写入 `foo.nix`: +```nix +{ lib, ... }: +lib.strings.removePrefix "I " "I see you!" +``` +在命令行将 `{ lib = (import {}).lib; }` 作为参数, +进行文件求值: ```bash -$ echo "x: x + 1" > file.nix -import ./file.nix 1 -2 +nix-instantiate --eval foo.nix --arg lib '(import {}).lib' +``` +运行结果与前面一例相同。 + +有时还会同时传入 `pkgs` 和 `lib`, +此时可以假设 `pkgs.lib` 与 `lib` 是等价的。 +这样做则是为了通过避免重复使用 `pkgs.lib` 来提高可读性。 + +示例: +```nix +{ pkgs, lib, ... }: +# ... 多次使用 `pkgs` +# ... 多次使用 `lib` ``` + + +::: + + +::: note +出于历史原因,`pkgs.lib` 中的一些函数与同名的 `builtins` 等价。 + + +:::