Nix 语言基础

Nix 表达式语言是一种纯函数式领域特定语言,用于描述软件包和系统配置。本文详解 Nix 的语法特性、数据类型、函数定义、惰性求值和模块系统。

1. Nix 语言简介

Nix 表达式语言(Nix Expression Language)是一门专门为软件包管理和系统配置设计的纯函数式编程语言。它的核心设计目标是实现可复现性(Reproducibility)和声明式配置(Declarative Configuration)——给定相同的 Nix 表达式,无论在何时何地求值,都会得到完全相同的结果。

与其他函数式语言(如 Haskell 或 Lisp)不同,Nix 语言刻意保持了极简性。它有且仅有以下数据类型:

  • 基础类型:字符串、整数、浮点数、布尔值、空值(null)
  • 复合类型:列表(list)、属性集(attribute set,类似字典/哈希表)
  • 函数类型:一等公民,可作为参数传递和返回值
  • 路径类型:专门处理文件路径

这种极简设计使得 Nix 表达式易于理解和审计,但也意味着某些在通用语言中常见的功能(如循环、迭代器、异常处理)需要以函数式的方式来表达。

2. 基本语法

2.1 表达式求值入门

# Nix 语言通过 nix 命令求值
nix eval --expr '1 + 2'        # 输出: 3
nix eval --expr '"hello"'      # 输出: "hello"
nix eval --expr '{ a = 1; b = 2; }.a'  # 输出: 1

2.2 变量绑定(let … in)

Nix 使用 let 表达式进行局部变量绑定:

let
  x = 10;
  y = 20;
  sum = x + y;
in
  sum * 2
# 结果: 60

let 绑定的一个重要特性是惰性求值——变量只在实际使用时才被求值。

2.3 属性集(Attribute Set)

属性集是 Nix 语言最核心的数据类型,类似于其他语言中的对象或字典:

let
  person = {
    name = "Alice";
    age = 30;
    skills = [ "Nix" "Haskell" "Rust" ];
    address = {
      city = "Beijing";
      country = "China";
    };
  };
in
  person.name
# 结果: "Alice"

使用点号或 ? 运算符访问属性:

let
  config = {
    enableFeature = true;
    port = 8080;
  };
in
  {
    hasFeature = config.enableFeature;
    hasTimeout = config.timeout or 30;  # 使用 or 提供默认值
    # 等价于: config.timeout or 30
  }

2.4 列表

Nix 的列表使用方括号表示,元素之间用空格分隔:

let
  numbers = [ 1 2 3 4 5 ];
  mixed = [ "hello" 42 true { key = "value"; } ];
in
  # 使用 builtin 函数操作列表
  builtins.head numbers      # 1 (第一个元素)
  builtins.tail numbers      # [ 2 3 4 5 ] (剩余元素)
  builtins.length numbers    # 5 (长度)
  builtins.elemAt numbers 2  # 3 (索引访问,从 0 开始)

2.5 字符串与插值

Nix 支持字符串插值,使用 ${} 语法:

let
  name = "Nix";
  version = "2.18";
in
  {
    singleLine = "Hello, ${name} ${version}!";
    
    # 多行字符串(保留缩进)
    multiLine = ''
      This is a ${name} expression.
      It supports multi-line strings
      without escaping newlines.
    '';
  }

字符串上下文(String Context)是 Nix 的独特概念——字符串可以携带依赖信息。这种机制使得 Nix 能够追踪字符串中引用的所有软件包依赖。

3. 函数定义与调用

3.1 基本函数

Nix 函数是一等公民,函数定义和调用语法非常简洁:

let
  # 定义一个函数: 参数 -> 函数体
  greet = name: "Hello, ${name}!";
  
  # 多参数函数使用柯里化
  add = x: y: x + y;
  
  # 属性集参数(自动解构)
  makePerson = { name, age, city ? "unknown" }:
    { inherit name age city; };
  
in
  {
    a = greet "World";          # "Hello, World!"
    b = add 10 20;             # 30
    c = makePerson { name = "Bob"; age = 25; };
    # { name = "Bob"; age = 25; city = "unknown"; }
  }

3.2 参数解构

属性集参数支持多种解构模式:

let
  # 使用 @ 语法捕获整个属性集
  greetPerson = args@{ name, age, ... }:
    {
      message = "Hello ${name}, you are ${toString age} years old";
      hasExtra = args ? extra;  # 检查是否有 extra 属性
    };

  # 剩余属性收集
  mergeConfig = defaults: user@{ ... }:
    defaults // user;
    # // 运算符用于合并两个属性集

in
  greetPerson { name = "Alice"; age = 30; extra = true; }

3.3 递归函数

使用 rec 关键字创建递归属性集:

let
  # 使用 builtins 的递归函数
  factorial = n:
    if n <= 1 then 1
    else n * factorial (n - 1);

  # rec 属性集:属性可以互相引用
  config = rec {
    name = "my-app";
    version = "1.0.0";
    fullName = "${name}-${version}";  # "my-app-1.0.0"
    dirName = "${fullName}-dist";       # "my-app-1.0.0-dist"
  };
  
in
  { 
    fact5 = factorial 5;  # 120
    appDir = config.dirName;
  }

4. 内置函数与库

4.1 常用内置函数

with builtins;  # 使 builtins 的函数可以直接使用

let
  list = [ 3 1 4 1 5 9 ];
  attrs = { a = 1; b = 2; c = 3; };
in
  {
    # 列表操作
    sorted = sort (a: b: a < b) list;           # [ 1 1 3 4 5 9 ]
    filtered = filter (x: x > 2) list;           # [ 3 4 5 9 ]
    mapped = map (x: x * 2) list;                # [ 6 2 8 2 10 18 ]
    reduced = foldl' (acc: x: acc + x) 0 list;   # 23
    
    # 属性集操作
    names = attrNames attrs;                      # [ "a" "b" "c" ]
    values = attrValues attrs;                    # [ 1 2 3 ]
    hasA = hasAttr "a" attrs;                     # true
    
    # 类型检查
    typeOfList = typeOf list;                     # "list"
    typeOfAttrs = typeOf attrs;                   # "set"
    
    # 路径操作
    base = baseNameOf /home/user/file.txt;        # "file.txt"
    dir = dirOf /home/user/file.txt;              # /home/user
    
    # 字符串操作
    split_ = split "," "a,b,c";                    # [ "a" "b" "c" ]
    concat = concatStringsSep "-" [ "a" "b" "c" ]; # "a-b-c"
  }

4.2 Nixpkgs 库函数

lib 模块提供了大量实用函数:

{ pkgs ? import <nixpkgs> {} }:

with pkgs.lib;

let
  data = {
    users = {
      alice = { home = "/home/alice"; shell = "bash"; };
      bob = { home = "/home/bob"; shell = "zsh"; };
    };
  };
in
  {
    # 递归遍历属性集
    allShells = collect isString data;  # [ "bash" "zsh" ]
    
    # 过滤映射
    shellUsers = filterAttrs (name: user: user.shell == "bash") data.users;
    
    # mapAttrs: 映射属性集
    homeDirs = mapAttrs (name: user: user.home) data.users;
    # { alice = "/home/alice"; bob = "/home/bob"; }
    
    # 条件合并
    config = recursiveUpdate
      { 
        services.nginx.enable = false;
        services.nginx.port = 80;
      }
      {
        services.nginx.enable = true;
      };
    # services.nginx = { enable = true; port = 80; }
    
    # 列表拉链
    pairs = zipListsWith (a: b: "${a}=${b}") [ "x" "y" ] [ "1" "2" ];
    # [ "x=1" "y=2" ]
  }

5. 导入与模块

5.1 文件导入

# config/default.nix
{
  name = "my-project";
  version = "2.0.0";
}

# default.nix
let
  config = import ./config/default.nix;
in
  "${config.name}-${config.version}"

5.2 带参数的导入

# mkDerivation.nix
{ stdenv, lib, name, src, buildPhase ? "make" }:

stdenv.mkDerivation {
  inherit name src;
  buildPhase = ''
    ${buildPhase}
  '';
}

# 使用
callPackage ./mkDerivation.nix {
  name = "hello";
  src = ./.;
  buildPhase = "gcc -o hello hello.c";
}

5.3 Nix 模块系统

NixOS 的模块系统是 Nix 语言最强大的应用之一:

# my-module.nix
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.myService;
in
{
  # 选项声明
  options.services.myService = {
    enable = mkEnableOption "my custom service";
    
    port = mkOption {
      type = types.int;
      default = 8080;
      description = "服务监听端口";
    };
    
    user = mkOption {
      type = types.str;
      default = "myuser";
      description = "运行服务的用户";
    };
  };

  # 配置实现
  config = mkIf cfg.enable {
    systemd.services.myService = {
      description = "My Service";
      serviceConfig = {
        ExecStart = "${pkgs.myService}/bin/my-service --port ${toString cfg.port}";
        User = cfg.user;
      };
    };
  };
}

6. 惰性求值与严格求值

Nix 语言默认是惰性求值的,这意味着表达式只有在实际需要时才会被求值:

let
  # infiniteList 会被定义但不会被求值
  # 因为我们没有访问它的任何元素
  infiniteList = [ 1 ] ++ infiniteList;

  # 无限递归的属性集也不会导致无限循环
  # 除非我们试图访问需要解引用的属性
  infiniteSet = {
    a = 1;
    b = infiniteSet.a + 1;  # 惰性:只在访问 b 时计算
  };

  # 使用 builtins.seq 强制求值
  strictResult = builtins.seq infiniteSet.a infiniteSet.a;  # 返回 1
in
  infiniteSet.a  # 只计算需要访问的部分

7. 条件表达式与模式匹配

let
  grade = score:
    if score >= 90 then "A"
    else if score >= 80 then "B"
    else if score >= 70 then "C"
    else if score >= 60 then "D"
    else "F";

  # 使用属性集的模式匹配
  processConfig = { enable ? false, port, ... }:
    if enable then { inherit port; active = true; }
    else { port = null; active = false; };

  # 使用 lib.switch 进行多分支匹配
  osName = os:
    pkgs.lib.switch os [ {
      case = "linux";
      value = "GNU/Linux";
    } {
      case = "darwin";
      value = "macOS";
    } ] "Unknown";

in
  {
    myGrade = grade 85;  # "B"
    myConfig = processConfig { port = 3000; enable = true; };
  }

8. 总结

Nix 表达式语言虽然小巧,但其纯函数式和惰性求值的设计使其非常适合描述复杂的软件包依赖和系统配置。核心要点:

概念Nix 表达说明
变量绑定let x = 1; in x + 2惰性求值
函数x: y: x + y柯里化
属性集{ a = 1; b = 2; }核心数据结构
列表[ 1 2 3 ]不可变
字符串插值"Hello, ${name}!"支持多行
递归rec { ... } 或自引用自引用属性集
默认值config.timeout or 30提供回退值
合并attrs1 // attrs2属性集合并

掌握这些基础后,接下来将学习如何使用 Nix 语言来管理软件包——这是 Nix 最经典的应用场景。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「devops」更多文章

  1. 值班工程与告警疲劳治理
  2. 基础设施代码测试:Terratest、Kitchen 与 InSpec
  3. 发布列车与版本节奏治理