Nix 表达式语言 介绍

Source

        近期在解决Nix包管理框架上一些问题,顺便整理一下关于Nix函数表达式语言的一些基础,本文涵盖 Nix 表达式语言的语法、语义、类型、编译、工具和库。

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

1. 语言的范式

    1) Lazy 惰性。用时触发定义和赋值。如下 a 没有被使用,所以可以认为a 不存在,abort lamda表达式从没有被调用过。

       let 

            a = abort “will never called”;

            b = “hi”;

             c = “world”;

       in b + c

    2)Functional 函数式

          函数式编程是一种构建计算机程序结构和元素的风格——将计算视为对数学函数的赋值,并避免使用变化的状态和可变的数据。 它是一种声明式编程范式,这意味着编程是用表达式或声明来实现,而不是语句。

    3)Pure 纯粹,函数调用干净无污染。

        纯函数是一种函数,其返回值仅由其输入值确定,而没有可观察到的副作用。 在 Nix 中,所有构建操作都试图尽可能纯粹以实现可重现的构建。 这意味着无论您在何处构建包,都应尽可能减少副作用对构建的影响。

2. 语言特点

    1)表达式

当 Nix 教程谈论 Nix 表达式时,它们通常是指具有多个输入的函数的定义,这些输入是推导的结果。然而,Nix 表达式可以是任何东西,从简单的字符串到函数再到一组表达式。

    2)类型

Type Description Example
Strings Strings either start with double quotes or double single quotes. They also support antiquotation (templating). Leading spaces are stripped with double single quotes. "Say ${pkgs.hello.name}"

Multiline String:

''first line
  second line
''
Integers A whole number without fractional component. 5
Floating-point numbers Decimal numbers. Precision is limited. 1.2
Path Relative paths will become absolute when evaluated, paths must contain a slash. <nixpkgs/pkgs> is also possible and will resolve to the folder incl. subfolders in your NIX_PATH
./hello/world
> /abs/path/to/hello/world
<nixpkgs/lib>
> /path/to/your/nixpkgs/lib
URI Uniform Resource Identifiers http://example.org/foo.tar.bz2
Boolean   truefalse
Null A representation of nothing. null
Lists Items are separated by space, not comma. Each item can be a value of any other type
[ 1 ./example.bin { hello="world"; }]
Sets Associative data structures. In other languages called dicts(Python),objects(JavaScript) hashes(Ruby) or maps(Java). Essentially a list of key-value pairs
{ key1="value1";  key2="value2"; }
Access values through dot-notation:
{ hello="world"; }.hello
> "world"
Functions See below. argument: function-body

        3)函数

函数都是具有以下符号的未命名 (=lambda) 函数:参数:nixExpression,例如 x:x*x。

如果你想给这个函数一个名字,你必须给它一个名字,例如 平方 = x:x*x。 所以,f(x) = x*x 在数学中是 f = x: x*x 在 Nix 中。

如果要使用该函数并将其应用于 f(3) 之类的值,请省略括号并添加一个空格。 因此,数学中的 f(3) 在 Nix 中是 f 3。

如果你想要多个参数,你可以添加这样的参数:arg1: arg2: nixExpression,例如 f = x: y: x*y。 将该函数应用于多个值很容易:数学中的 f(3,4),在 Nix 中是 f 3 4。 如果仅应用一个参数 f 3,则返回偏函数 y: 3*yi。

         a. 解构
在 nix 中,集合是作为函数的参数给出的。 假设我们声明了一个函数,它返回集合的属性 a 和 b 的串联,例如:

concat_a_and_b =  set: set.a + set.b 
 concat_a_and_b { a="hello"; b="world"; }
"helloworld"

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

 concat_a_and_b = {a, b}: a + b
concat_a_and_b { a="hello "; b="world"; }
"hello world"

           b. 默认参数
如果调用者省略一个,可以指定在函数中使用的默认值,但只能作为集合的一部分。

add_a_b = { a ? 1, b ? 2 }: a + b
add_a_b {}
3
add_a_b {a=5;}
7

           c. 接受参数集中的意外属性
如果您希望您的函数在用户提供的属性比您预期的要多的情况下仍能正常运行,您可以使用省略号。

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

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

add_a_b = args@{ a, b, ... }: a + b + args.c
add_a_b { a=5; b=2; c=10; }
17

     4. 操作符及优先级
较小的优先级数字意味着更强的绑定; 即这个列表从最强绑定到最弱绑定排序,并且在两个运算符之间优先级相等的情况下,结合性决定绑定。

 

Prec Abbreviation Example Assoc Description
1 SELECT e . attrpath [or def] none Select attribute denoted by the attribute path attrpath from set e. (An attribute path is a dot-separated list of attribute names.) If the attribute doesn’t exist, return default if provided, otherwise abort evaluation.
2 APP e1 e2 left Call function e1 with argument e2.
3 NEG -e none Numeric negation.
4 HAS_ATTR e ? attrpath none Test whether set e contains the attribute denoted by attrpath; return true or false.
5 CONCAT e1 ++ e2 right List concatenation.
6 MUL e1 * e2 left Numeric multiplication.
6 DIV e1 / e2 left Numeric division.
7 ADD e1 + e2 left Numeric addition, or string concatenation.
7 SUB e1 - e2 left Numeric subtraction.
8 NOT !e left Boolean negation.
9 UPDATE e1 // e2 right Return a set consisting of the attributes in e1 and e2 (with the latter taking precedence over the former in case of equally named attributes).
10 LT e1 < e2 left Less than.
10 LTE e1 <= e2 left Less than or equal.
10 GT e1 > e2 left Greater than.
10 GTE e1 >= e2 left Greater than or equal.
11 EQ e1 == e2 none Equality.
11 NEQ e1 != e2 none Inequality.
12 AND e1 && e2 left Logical AND.
13 OR e1 || e2 left Logical OR.
14 IMPL e1 -> e2 none Logical implication (equivalent to !e1 || e2).

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

用法:

 x = import <nixpkgs> {};
 y = trace x.pkgs.hello.name x;

     6. 一些重要的构造函数
Nix 看起来很像带有函数的 JSON,但也提供了许多非常专业的构造函数,可以帮助您构建清晰易读的表达式。 在本子章节中,将通过示例展示最显着的结构:

           a. with 
with 语句(链接到 Nix 手册部分)将 attrset 的值内容引入到后续表达式的词法范围中。 这意味着它将该集合中的所有键(在外部作用域中尚不存在)带入该表达式的作用域中。 因此,您不需要使用点表示法。

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 不会覆盖外部作用域中的值。 例如:

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

returns:

"In this string we have access to 333 and 2"

这是因为 a 已经在 with 使用之前已定义,并且 with 不会覆盖它。 外部值优先。 (这个wiki部分的作者怀疑这种非阴影逻辑的原因是词法代码稳定性:在如下所示的with的常见用法中,赋予语句的attrsets内容往往很大,社区维护 , 并且经常更新。如果例如 let myValue = ...; with lib; doSomethingWith myValue 遮蔽了外部 myValue 绑定,维护 `lib` 的人可能会通过添加 lib.myValue 意外破坏此代码。)

常见用法有:

在表达式之上:

看下面的lib集

         b.  let ... in 

使用 let 您可以定义局部变量,这些变量也可以引用 self 而无需 rec 构造。此功能在表达式中用于准备成为输出一部分的变量。 let 的用法类似于 Haskell let 表达式

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

         c. inherit 

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

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

buildPythonPackage rec {
  pname = "hello";
  version = "1.0";
  src = fetchPypi {
    inherit pname version;
   sha256 = "01ba..0";
  };
}

 

          d. rec 语句

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

rec {
  x = y - 100;
  y = 123;
}.x
=> 23