UP | HOME

快速入门

Table of Contents

交互模式

以下交互式教程需要使用 nix repl 命令调出交互命令模式:

$ nix repl
Welcome to Nix 2.5.1. Type :? for help.

它有点像用于调试 JavaScript 的控制台或 Python 的交互模式

nix-repl> 1 + 2  # 输入表达式
3  # 输出结果

即时计算

nix-repl> { a.b.c = 1; }
{ a = { ... }; }
在上面的例子中,我们输入了一个匿名集合,而这个匿名集合包含 a 集合 

匿名集合

匿名集合是没有分配名称的集合,与之对立的是命名集合,例如 foo = { bar };

a 集合中的值并没有被这个匿名集合直接依赖

自然顶级以下的集合不会被立刻求值,占位的变成了 ... 

在下面这个例子,将显式声明 qux 的直接依赖:

let
  foo = { bar.qux = 1; };
  lax = foo.bar.qux;
in
lax  # 我们需要 lax,lax 需要 foo.bar.qux

惰性求值

Nix语言的求值是 惰性的 ,这意味着 表达式不会在被绑定到变量后立即求值,而是在该值 被使用 时才求值

可以输入 :p 启用 即刻求值 ,所有表达式都将被立刻求值:

nix-repl> :p { a.b.c = 1; }
{ a = { b = { c = 1; }; }; }

注意:

  • :p 参数 只能交互 模式使用
  • 输入 :q 可以 退出 交互模式

文件求值

使用 nix-instantiate –evalnix 文件 中存在的 表达式 进行 求值

echo 1 + 2 > file.nix  # 该命令会往 file.nix 中写入 1 + 2
nix-instantiate --eval file.nix  # 文件求值
3  # 输出结果

立即求值

在文件求值的情景下可以通过在命令行添加 –strict 参数来启用立即求值:

echo "{ a.b.c = 1; }" > file.nix
nix-instantiate --eval --strict file.nix
{ a = { b = { c = 1; }; }; }

echo 命令

echo 是 Linux 中最常见的命令之一,主要作用是输出文本,追加文本,返回输出

代码风格

好的代码风格会让程序员身心愉悦,同时也增加了代码可维护性

格式化

Alejandra 是一个新兴的 Nix 代码格式化工具,使用 Rust 编写

当心空格

空格用于分隔 词法标记 Lexical tokens ,在一些场景是必要的,不然会无法区分关键字

在许多中文资料中,混淆了 Lexical,Syntax 和 Grammar 三者的概念:

Lexical(词法):是指语言中单词的意义、形态和用法等方面的规则。词法规则定义了单词的基本形态和语法功能,例如名词、动词、形容词等。同时,它还规定了一些特殊单词的用法,例如冠词、介词、连词等

Syntax(句法):是指语言中标记(Token)之间的组合方式,以及这种组合方式所遵循的规则。通俗点说,语法规定了单词应该如何排列、组合成句子,以及这些句子之间的联系方式

Grammar(语法):是指语言中的规则体系,包括了语法规则、语义规则和语用规则等。它涉及到语言的整个结构和组成方式,而不仅仅是句子的构成

下面的两种示例是等价的:

let
  x = 1;
  y = 2;
in
x + y

显然,下面的可读性比上面的差很多:

let x=1;y=2;in x+y

名称和值

原始数据类型列表属性集函数 都可以被当作 ,可以使用 = 为名称 绑定 值,然后用 分号 分隔 赋值 语句:

let
  foo = "I am a fool";
  bar = "I am at the bar";
in
foo + bar

名称不等同常见编程语言中的变量,因为它 一旦定义就无法修改 。在概念上,它们更多地是形成了一种 绑定 关系:

  • 一个值可以被多个名称绑定,一个名称只能绑定一个值

    这种赋值没有副作用
    
    传统的赋值会改变变量的状态,Nix 语言中的变量一旦赋值无法改变
    

属性集

还记得我们在上面提到的集合吗?

其实它真正的名字是属性集,没有过早引入属性集的概念是为了方便读者渐进式地理解

属性集 就是 装载若干对名称与值的集合

  • 集合内的 名称 被称为这个集合的 属性
  • 集合内中由 名称和值 组成的对则被称为该属性的 元素

    {
      string = "hello";
      integer = 1;
      float = 3.141;
      bool = true;
      null = null;
      list = [ 1 "two" false ];
      attribute-set = {
        a = "hello";
        b = 2;
        c = 2.718;
        d = false;
      };  # 标准 json 不支持注释
    }
    

    可能觉得莫名的像 json,下面是 json 的示例:

    {
        "string": "hello",
        "integer": 1,
        "float": 3.141,
        "bool": true,
        "null": null,
        "list": [1, "two", false],
        "object": {
            "a": "hello",
            "b": 1,
            "c": 2.718,
            "d": false
        }
    }
    

    注意:

  • 属性不需要添加引号
  • 列表是用空格分隔的

递归属性集

当属性集内的属性需要访问该集合的另一个属性时,应当使用 递归属性集

rec {
  one = 1;
  two = one + 1;  # 直接依赖于 one
  three = two + 1; # 直接依赖于 two,间接依赖于 one
}

输出如下:

{ one = 1; three = 3; two = 2; }
元素的声明顺序并不决定元素在属性集中的排布顺序

属性集中的元素排布顺序是由求值顺序决定的,优先被求值的被放在了前面

let 绑定

一个完整的 let 绑定有两个部分:

  1. let: 绑定名称与值
  2. in: 使用名称

在 let 与 in 之间的语句中,可以声明需要被复用的名称,并将其与值绑定。它们可以在 in 之后的表达式 中发挥作用:

let
  b = a + 1;
  a = 1;
in
a + b

引用到 a 的地方有两处,它们都会将 a 替换 成值来计算或赋值,类似于常量

不需要关心名称的声明顺序,不会出现名称未定义的情况

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

x 未定义:

error: undefined variable 'x'

at «string»:3:7:

2|   a = let x = 1; in x;
                       3|   b = x;
                                 |       ^
                                 4| }

属性访问

使用 . 访问属性:

let
  attrset = { x = 1; };
in
attrset.x

访问嵌套的属性也是同样的方式:

let
  attrset = { a = { b = { c = 1; }; }; };
in
attrset.a.b.c

当然,就像如何访问属性一样,也可以用 . 直接赋值它:

let
  a.b.c = 1;
in
a.b.c

with 表达式

with 表达式可以让人少写几次属性集的名称,是个语法糖:

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
with a; [ x y z ]  # 等价 [ a.x a.y a.z ]

作用域被限制到了 分号后面的第一个表达式 内:

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
{
  b = with a; [ x y z ];
  c = x;  # a.x
} 

x 未定义:

error: undefined variable 'x'

at «string»:10:11:

9|       b = with a; [ x y z ];
10|       c = x;  # a.x
                             |           ^
                             11|     }

inherit 表达式

inherit 本意就是继承,可以使用它完成 一对命名相同的名称和属性之间的赋值

let
  x = 1;
  y = 2;
in
{
  inherit x y;
}

没有这个语法糖,可能得这样写:

let
  x = 1;
  y = 2;
in
{
  x = x;
  y = y;
}

加上括号,就直接从属性集继承名称:

let
  a = { x = 1; y = 2; };
in
{
  inherit (a) x y;
}

inherit 同样可以在 let 表达式中使用:

let
  inherit ({ x = 1; y = 2; }) x y;
in
[ x y ]

等价于:

let
  x = { x = 1; y = 2; }.x;
  y = { x = 1; y = 2; }.y;
in
[ x y ]
变相的将特定属性带到了全局作用域,实现了更方便的解构出名称的方法

字符串插值

各大流行语言均已支持,使用 ${ … } 可以 插入 名称的值:

let
  name = "Nix";
in
"hello ${name}"

输出为:

"hello Nix"

字符串插值语法 只支持字符串类型 ,所以引入的名称的值必须是字符串,或是可以转换为字符串的类型:

let
  x = 1;
in
"${x} + ${x} = ${x + x}"

因为是数字类型,所以报错:

error: cannot coerce an integer to a string

at «string»:4:2:

3| in
4| "${x} + ${x} = ${x + x}"
                         |  ^
                         5|

字符串插值是可以 被嵌套 的:

let
  a = "no";
in
"${a + " ${a + " ${a}"}"}"

输出为:

"no no no"

路径类型

路径 在 Nix 语言中不是字符串类型,而是一种 独立的 类型 ,以下是一些路径的示例:

./relative  # 当前文件夹下 relative 文件(夹)的相对路径
/current/directory/absolute  # 绝对路径,从根目录开始指定
../  # 当前目录的上级目录
../../  # 当前目录的上级的上级目录
./  # 当前目录

检索路径

这又被称为“尖括号语法”

检索路径 是通过 系统变量获取 路径 的语法,由 一对尖括号 组成:

<nixpkgs>

这个时候 <nixpkgs> 实际上一个依赖了系统变量中为 $NIX_PATH 的路径值:

/nix/var/nix/profiles/per-user/root/channels/nixos

建议: 避免 使用检索路径来指定其它相对路径,比如下面的例子:

<nixpkgs/lib>

这是一种 污染 ,因为这样指定相对路径会让配置与环境产生联系

配置文件应该尽量保留纯函数式的特性,即输出只与输入有关,纯函数不应该与外界产生任何联系

字符串

多行字符串

Nix 中被 两对单引号 '' 引用的内容即为多行字符串:

''
multi
line
string
''

等价于:

"multi\nline\nstring"

Nix 的多行字符串存在特殊行为,Nix 会智能地去除掉开头的缩进,这在其他语言中是不常见的:

''
  one
   two
    three
''

等价于:

"one\n two\n  three\n"

函数

函数在 Nix 语言中是人上人,先来声明一个 匿名函数 Lambda

x: x + 1

  #  «lambda @ «string»:1:1»
  • 引号左边是函数 参数
  • 引号右边跟随一个 空格 ,随即是 函数体

Nix 支持多重参数(柯里化函数):

x: y: x + y

  #«lambda @ «string»:1:1»

参数当然可以是 属性集 类型:

{ a, b }: a + b

  #«lambda @ «string»:1:1»

为函数指定 默认 参数,在缺省该参数赋值的情况下,它就是默认值:

{ a, b ? 0 }: a + b

允许传入额外的属性:

{ a, b, ...}: a + b  # 明确传入的属性有 a 和 b,传入额外的属性将被忽略
  { a, b, ...}: a + b + c  # 即使传入的属性有 c,一样不会参与计算,这里会报错 

    #  error: undefined variable 'c'

    #      at «string»:1:23:

    #          1| { a, b, ...}: a + b + 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

  # 2 

高阶函数

将 \(f (a,b,c)\) 转换为 \(f (a)(b)(c)\) 的过程就是 柯里化

为什么需要柯里化?
  • 它很灵活,可以避免重复传入参数
  • 当传入第一个参数的时候,该函数就已经具有了第一个参数的状态 闭包

尝试声明一个柯里化函数:

x: y: x + y

  #«lambda @ «string»:1:1»

为了更好的可读性:

x: (y: x + y)

这个例子中的柯里化函数,虽然接收两个参数,但不是 迫切 需要:

let
  f = x: (y: x + y)
in
f 1

输出为:

«lambda @ «string»:1:13»

\(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; b = 2; }

额外参数

有的时候,设计的函数不得不接收一些暂时不需要的额外参数,可以使用 允许接收额外参数:

{a, b, ...}: a + b

这次不会报错:

let
  f = {a, b, ...}: a + b;
in
f { a = 1; b = 2; c = 3; }

命名参数集

假如需要使用某个额外参数,可以使用 命名属性集 将其 接收 到一个另外的集合:

let
  f = {a, b, ...}@args: a + b + args.c;
in
f { a = 1; b = 2; c = 3; }

函数库

除了一些内建操作符 (+, ==, &&, 等),还要学习一些被视为事实标准的库

内建函数

它们在 Nix 语言中并不是 <LAMBDA> 类型,而是 <PRIMOP> 元操作类型 primitive operations

builtins.toString  # 通过 builtins 使用函数
这些函数是内置在 Nix 解释器中,由 C++ 实现

查询内建函数以了解其使用方法

导入

import 表达式以 其他 Nix 文件的路径 为参数, 返回 该 Nix 文件的求值结果

  • import 的参数如果为 文件夹路径 ,那么会返回该文件夹下的 default.nix 文件的执行结果

如下示例中,import 会导入 ./file.nix 文件,并返回该文件的求值结果:

echo 1 + 2 > file.nix

import ./file.nix

3

被导入的 Nix 文件可以返回 任何内容 ,返回值可以像上面的例子是 数值 ,也可以是 属性集函数列表 ,等等:

echo "x: x + 1" > file.nix
import ./file.nix 1
2
Next: 进阶手册 Home: Nix 语言