快速入门
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 –eval 对 nix 文件 中存在的 表达式 进行 求值 :
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 绑定有两个部分:
- let: 绑定名称与值
- 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 语言 |