Nix 表达式语言是一种纯粹的、惰性的、函数式的语言。纯度意味着语言中的操作没有副作用(例如,没有变量赋值)。惰性意味着函数的参数仅在需要时才被评估。函数式意味着函数是“正常”值,可以以有趣的方式传递和操作。该语言不是功能齐全的通用语言,它的主要工作是描述包、包的组成以及包内的可变性。 – 来自Nix 手册

简而言之,该语言是专门为 Nix 包管理器设计的。

特色

惰性 (延迟初始化)

并非 nixpkgs 中的所有表达式都将被评估和实例化,因为 nix 仅在完成输出需要时执行评估。在以下示例中,由于 abort 所属的变量未使用,因此永远不会被触发:

1
2
3
4
5
let
  a = abort "will never happen";
  b = "hello";
  c = "world";
in 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
2
3
4
5
6
# 声明函数
concat_a_and_b = set: set.a + set.b
# 调用函数
concat_a_and_b { a="hello"; b="world"; }
# 返回结果
"helloworld"

然后可以在函数声明中解构参数 set 并挑选出我们感兴趣的属性,从而产生一个更整洁的函数:

1
2
3
4
5
6
# 声明函数
concat_a_and_b = {a, b}: a + b
# 调用函数
concat_a_and_b { a="hello"; b="world"; }
# 返回结果
"helloworld"

参数默认值

如果调用者省略一个,也可以指定要在函数中使用的默认值,但仅作为集合的一部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 声明函数并设置参数默认值
add_a_b = { a ? 1, b ? 2 }: a + b

# 调用函数 (不传参数)
add_a_b {}
# 返回结果
3

# 调用函数 (传参数)
add_a_b {a=5;}
# 返回结果
7

参数集中的意外属性

如果希望函数在用户提供的属性集比您预期的更多的情况下仍然运行而不会出错,可以使用省略号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 错误示范
add_a_b = { a, b }: a + b
add_a_b { a=5; b=2; c=10; }
# 返回错误
error: anonymous function at (string):1:2 called with unexpected argument 'c', at (string):1:1

# 正确示范
add_a_b = { a, b, ... }: a + b
add_a_b { a=5; b=2; c=10; }
# 返回结果
7

您还可以使用 @ 模式将参数存储在指定的名称中。

1
2
3
4
add_a_b = args@{ a, b, ... }: a + b + args.c
add_a_b { a=5; b=2; c=10; }
# 返回结果
17

导入

import 加载、解析和导入存储在 path 中的 nix 表达式。这个关键字本质上是 nix 的内置函数,但不是语言本身的一部分。

用法:

1
2
x = import <nixpkgs> {};
y = 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 表示的属性;返回 truefalse
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 返回由 e1e2 中的属性组成的集合(如果属性名称相同,则后者优先于前者)
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 的值内容引入到后面表达式的词法范围中。这意味着它将该集合中的所有键(不存在于外部作用域中)引入该表达式的作用域中。所以,你不需要使用点符号。这在部分情况下能够在不影响可读性的前提下简化代码

例子:

1
2
3
4
5
6
7
let
  myattrset = { a = 1; b = 2; };
in
  with myattrset; "In this string we have access to ${toString a} and ${toString b}"

# 返回结果
"In this string we have access to 1 and 2"

注意 with 不会覆盖外部作用域的值。见如下实例:

1
2
3
4
5
6
7
let
  a = 333;
in
  with { a = 1; b = 2; }; "In this string we have access to ${toString a} and ${toString b}"

# 返回结果
"In this string we have access to 333 and 2"

这种 非阴影逻辑 使得 Nix 与常见的编程语言行为有所不同,使用时应多加留意。

常见用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 不使用 with
{pkgs}:
{
  ...
  buildInputs = with pkgs; [ curl php coreutils procps ffmpeg ];
}

# 使用 with
{pkgs}:
{
  ...
  buildInputs = [ pkgs.curl pkgs.php pkgs.coreutils pkgs.procps pkgs.ffmpeg ];
}

let … in 语句

let 可以定义局部变量,这些变量也可以引用自我(无需 rec 声明)。 这一特性在表达式中被用来准备成为输出的一部分的变量。

1
2
3
4
5
let
  a = 1;
  b = 2;
in  a + b
=> 3

rec 语句

rec 声明将基本集合转换为可以进行自引用的集合。 当 let 表达式会造成太多混乱时,可以使用它。它经常出现在包派生描述中。

示例用法:

1
2
3
4
5
rec {
  x = y - 100;
  y = 123;
}.x
=> 23

inherit 语句

继承表达式可用于从周围的词法范围复制变量。 一个典型的用例是在表达式中声明派生的版本或名称,并在函数中重用此参数来获取源。

这是一个典型的 python 包派生,因为 fetchPypi 函数还需要 pnameversion 作为输入:

1
2
3
4
5
6
7
8
buildPythonPackage rec {
  pname = "hello";
  version = "1.0";
  src = fetchPypi {
    inherit pname version;
   sha256 = "01ba..0";
  };
}

参考资料

官方 Wiki: Overview of the Nix Language