Nix 语言概述
Nix 表达式语言是一种纯粹的、惰性的、函数式的语言。纯度意味着语言中的操作没有副作用(例如,没有变量赋值)。惰性意味着函数的参数仅在需要时才被评估。函数式意味着函数是“正常”值,可以以有趣的方式传递和操作。该语言不是功能齐全的通用语言,它的主要工作是描述包、包的组成以及包内的可变性。 – 来自Nix 手册
简而言之,该语言是专门为 Nix 包管理器设计的。
特色
惰性 (延迟初始化)
并非 nixpkgs 中的所有表达式都将被评估和实例化,因为 nix 仅在完成输出需要时执行评估。在以下示例中,由于 abort
所属的变量未使用,因此永远不会被触发:
1let
2 a = abort "will never happen";
3 b = "hello";
4 c = "world";
5in b + c
函数式
函数式编程是一种构建计算机程序结构和元素的风格——将计算视为对数学函数的评估,并避免改变状态和可变数据。它是一种声明式编程范式,这意味着编程是使用表达式或声明而不是语句来完成的。
纯函数
纯函数是返回值仅由其输入值决定的函数,没有可观察到的副作用。在 Nix 中,所有构建操作都试图尽可能地纯粹以实现可重现的构建。这意味着无论您在哪里构建包,尽可能少的副作用应该对构建产生影响。
基础语法
表达式
当 Nix 教程谈论 Nix 表达式时,它们通常是指具有多个输入的函数的定义,从而导致推导。然而,Nix 表达式可以是一切,从简单的字符串到函数再到一组表达式。
类型
nix 语言提供了许多基本类型:
类型 | 描述 | 例子 |
---|---|---|
Strings | 字符串以“双引号”或“双单引号”开头。 它们还支持反引用(模板)。 前导空格用双单引号去除。 | "Say ${pkgs.hello.name}" |
Integers | 没有小数部分的整数。 | 1 |
Floating-point numbers | 小数,精度有限。 | 1.2 |
Path | 相对路径在评估时将变成绝对路径,路径必须包含一个斜线。<nixpkgs/pkgs> 也是可以的,它将解析到你的NIX_PATH 中的文件夹,包括子文件夹。 |
./hello/world > /abs/path/to/hello/world <nixpkgs/lib> > /path/to/your/nixpkgs/lib |
URI | 统一资源标识符 | http://example.org/foo.tar.bz2 |
Boolean | true, false | |
Null | null | |
Lists | 项目之间用空格分隔。每个项目可以是任何类型的值。 | [ 1 ./example.bin { hello=“world”; }] |
Sets | 关联的数据结构。在其他语言中称为dicts (Python)、objects (JavaScript)、hash (Ruby)或maps (Java)。本质上是一个键值对的列表。 |
|
Functions | 详见下文 | argument: function-body |
所有类型的详细描述都可以在 NIX 手册 中找到。
函数
函数都是具有以下符号的未命名 (=lambda) 函数:argument: nixExpression
,例如 x: x*x
。可以通过 square = x: x*x
给函数一个名字。
如果您想使用该函数并将其应用于类似 的值f(3),则省略括号并添加一个空格。所以,f(3)在数学中,f 3在 Nix 中。
如果想要多个参数,可以通过 arg1: arg2: nixExpression
语法实现,例如 f = x: y: x*y
。
解构 (Destructuring)
在 nix 中,集合是作为函数的参数给出的。 假设我们声明了一个函数,它返回一个集合的属性 a 和 b 的连接,例如:
1# 声明函数
2concat_a_and_b = set: set.a + set.b
3# 调用函数
4concat_a_and_b { a="hello"; b="world"; }
5# 返回结果
6"helloworld"
然后可以在函数声明中解构参数 set
并挑选出我们感兴趣的属性,从而产生一个更整洁的函数:
1# 声明函数
2concat_a_and_b = {a, b}: a + b
3# 调用函数
4concat_a_and_b { a="hello"; b="world"; }
5# 返回结果
6"helloworld"
参数默认值
如果调用者省略一个,也可以指定要在函数中使用的默认值,但仅作为集合的一部分。
1# 声明函数并设置参数默认值
2add_a_b = { a ? 1, b ? 2 }: a + b
3
4# 调用函数 (不传参数)
5add_a_b {}
6# 返回结果
73
8
9# 调用函数 (传参数)
10add_a_b {a=5;}
11# 返回结果
127
参数集中的意外属性
如果希望函数在用户提供的属性集比您预期的更多的情况下仍然运行而不会出错,可以使用省略号。
1# 错误示范
2add_a_b = { a, b }: a + b
3add_a_b { a=5; b=2; c=10; }
4# 返回错误
5error: anonymous function at (string):1:2 called with unexpected argument 'c', at (string):1:1
6
7# 正确示范
8add_a_b = { a, b, ... }: a + b
9add_a_b { a=5; b=2; c=10; }
10# 返回结果
117
您还可以使用 @
模式将参数存储在指定的名称中。
1add_a_b = args@{ a, b, ... }: a + b + args.c
2add_a_b { a=5; b=2; c=10; }
3# 返回结果
417
导入
import 加载、解析和导入存储在 path 中的 nix 表达式。这个关键字本质上是 nix 的内置函数,但不是语言本身的一部分。
用法:
1x = import <nixpkgs> {};
2y = trace x.pkgs.hello.name x;
操作符
较低的优先级意味着更强的绑定; 即这个列表是从最强到最弱的绑定排序的,并且在两个运算符之间的优先级相等的情况下,关联性决定了绑定。
Prec | 缩写 | 例子 | 简称 | 描述 |
---|---|---|---|---|
1 | SELECT | e . attrpath [or def] | none | 从集合 e 中选择由属性路径 attrpath 表示的属性。(属性路径是一个点分隔的属性名称列表。)如果属性不存在,返回 default (如果提供的话),否则中止评估 |
2 | APP | e1 e2 | left | 用参数 e2 调用函数 e1 |
3 | NEG | -e | none | |
4 | HAS_ATTR | e ? attrpath | none | 测试集合 e 是否包含由 attrpath 表示的属性;返回 true 或 false |
5 | CONCAT | e1 ++ e2 | right | 列表串联 |
6 | MUL | e1 * e2 | left | |
6 | DIV | e1 / e2 | left | |
7 | ADD | e1 + e2 | left | |
7 | SUB | e1 - e2 | left | |
8 | NOT | !e | left | |
9 | UPDATE | e1 // e2 | right | 返回由 e1 和 e2 中的属性组成的集合(如果属性名称相同,则后者优先于前者) |
10 | LT | e1 < e2 | left | |
10 | LTE | e1 <= e2 | left | |
10 | GT | e1 > e2 | left | |
10 | GTE | e1 >= e2 | left | |
11 | EQ | e1 == e2 | none | |
11 | NEQ | e1 != e2 | none | |
12 | AND | e1 && e2 | left | |
13 | OR | e1 | e2 | |
14 | IMPL | e1 -> e2 | none | 逻辑蕴涵(等价于 !e1 |
常用语句
Nix 看起来很像带有函数的 JSON,但也提供了一些非常专业的语句,可以帮助你建立干净和容易阅读的代码。在本章节,将通过实例展示最值得注意的语句。
With 语句
with 语句将一个 attrset 的值内容引入到后面表达式的词法范围中。这意味着它将该集合中的所有键(不存在于外部作用域中)引入该表达式的作用域中。所以,你不需要使用点符号。这在部分情况下能够在不影响可读性的前提下简化代码。
例子:
1let
2 myattrset = { a = 1; b = 2; };
3in
4 with myattrset; "In this string we have access to ${toString a} and ${toString b}"
5
6# 返回结果
7"In this string we have access to 1 and 2"
注意 with 不会覆盖外部作用域的值。见如下实例:
1let
2 a = 333;
3in
4 with { a = 1; b = 2; }; "In this string we have access to ${toString a} and ${toString b}"
5
6# 返回结果
7"In this string we have access to 333 and 2"
这种 非阴影逻辑
使得 Nix 与常见的编程语言行为有所不同,使用时应多加留意。
常见用法:
1# 不使用 with
2{pkgs}:
3{
4 ...
5 buildInputs = with pkgs; [ curl php coreutils procps ffmpeg ];
6}
7
8# 使用 with
9{pkgs}:
10{
11 ...
12 buildInputs = [ pkgs.curl pkgs.php pkgs.coreutils pkgs.procps pkgs.ffmpeg ];
13}
let … in 语句
let 可以定义局部变量,这些变量也可以引用自我(无需 rec 声明)。 这一特性在表达式中被用来准备成为输出的一部分的变量。
1let
2 a = 1;
3 b = 2;
4in a + b
5=> 3
rec 语句
rec 声明将基本集合转换为可以进行自引用的集合。 当 let 表达式会造成太多混乱时,可以使用它。它经常出现在包派生描述中。
示例用法:
1rec {
2 x = y - 100;
3 y = 123;
4}.x
5=> 23
inherit 语句
继承表达式可用于从周围的词法范围复制变量。 一个典型的用例是在表达式中声明派生的版本或名称,并在函数中重用此参数来获取源。
这是一个典型的 python 包派生,因为 fetchPypi 函数还需要 pname
和 version
作为输入:
1buildPythonPackage rec {
2 pname = "hello";
3 version = "1.0";
4 src = fetchPypi {
5 inherit pname version;
6 sha256 = "01ba..0";
7 };
8}
参考资料
官方 Wiki: Overview of the Nix Language