Useful Nix

Введение

Чтобы попробовать примеры кода отсюда, вам необходим установленный Nix. Введите в консоли nix repl и пробуйте! Если вы хотите запускать фалы с кодом, вам пригодится команда nix-instantiate --eval file.nix.

В некоторых случаях такой запуск файла может дать вывод наподбие { a = <CODE>; }. Если это происходит, перед флагом --eval добавьте флаг --strict.

Nix - чисто функциональный язык программирования. В нем нет понятия последовательного выполнения шагов - любые зависимости от операций выполняются только через данные, полученные от других операций. Говоря простым языком, порядок не важен. Например:

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

Здесь a и b - просто выражения. Nix смотрит: нам нужен b. Чтобы вычислить его, нужно знать a. Он сам понимает это и вычисляет только то, что нужно и только тогда, когда нужно.

А вот в императивных языках программирования все по-другому: ты пишешь инструкции одну за другой, и они выполняются по порядку. Рассмотрим пример на Golang:

package main

import "fmt"

func main() {
	x := 1
	y := x + 2

	fmt.Println(y)
}

Сначала выполнится первая строка, затем вторая, и так далее. Порядок важен.

Разберем пример потруднее. Здесь используется функция builtins.throw - вызывает ошибку, если до нее дойдет вычисление.

let
  attrs = {
    a = 15;
    b = builtins.throw "Оу нет!";
  };
in "The value of 'a' is ${toString attrs.a}"

attrs - набор атрибутов (что то вроде словаря или объекта), у него есть два поля - a и b. Но в основной части с in вы используем только a, поэтому "ошибочное" значение b не вычисляется. Если мы добавим b в вычисляемую область, мы увидим вывод error: Оу нет!.

Помимо этого, любой фрагмент Nix кода является выражением, возращающим значение. Оценка (вычисление) выражения дает одну структуру данных, а не последовательность операций. Каждый файл Nix вычисляется в одно выражение.

Также Nix является "ленивым" языком. Мы уже рассмотрели выше пример с throw, когда он не вычисляется, ведь не используется в программе. Это и называется ленивостью.

Nix создан для конкретной цели - для взаимодействия с менеджером пакетов Nix. Хотя иногда его используют для других задач, он не является языком общего назначения.

Языковые конструкции

Здесь я расскажу о конструкциях в Nix. Это небольшой язык, и большинство из них очевидны.

Основные типы данных

# Числа
42
1.72394

# Пути
./somefolder/somefile.json
./.
/etc
~/.config

# Строки
"hello"


# Интерполяция строк
"Hello ${name}"

# Многострочный литерал
''
  first line
  second line
''

# Нулевое значение
null

# Булевы значения
true, false

# Списки
[ 1 2 3 ] # Обратите внимание: без запятых

# Наборы атрибутов (словари/объекты)
{ a = 15; b = "something else"; }
{ foo.bar = 1; } # { foo = { bar = 1; }; }

# Рекурсивные наборы атрибутов (вы можете использовать значения на месте)
rec { a = 15; b = a * 2; }

Операторы

ОператорОписание
+, -, *, /Стандартные арифметические операции
+Конкатенация строк
++Объединение списков
==, !=Логическое равенство/неравенство
>, >=, <, <=Логические операции сравнения
&&, ||Логические AND и OR
->Логическая импликация
!Отрицание
set.attrДоступ к атрибуту attr из набора set
set ? attributeПроверяет, содержит ли набор атрибут
left // rightОбъединяет два набора атрибутов

Оператор объединения //

Этот оператор широко используется в Nix коде, поэтому вам желательно изучить его. В других языках таких операторов почти не бывает.

Он объединяет два набора атрибутов, переданных ему:

{ a = 1; } // { b = 2; }
# Возвращает { a = 1; b = 2; }

Значение справа от оператора имеют больший приоритет:

{ a = "left"; } // { a = "right"; }
# { a = "right"; }

Этот оператор не умеет рекурсивно объединять списки:

{ a = { b = 1; }; } // { a = { c = 2; }; }
# { a = { c = 2; }; }

Вместо этого используйте lib.mkMerge:

lib.mkMerge [
  { a = { b = 1; }; }
  { a = { c = 2; }; }
];
# { a = { b = 1; c = 2; }; }

Переменные

В Nix переменные вводятся с помощью выражения let ... in. Они неизменяемы и доступны только в области действия этого выражения. Глобальных переменных нет.

let
  a = 15;
  b = 2;
in a * b
# 30

Функции

Все функции в Nix являются лямбда-функциями. Это означает, что они обрабатываются как данные. Давать им имена можно с помощью присваивания их переменным или устанавливая их в качестве значений набора атрибутов.

Объявление функции - это просто единственный аргумент, за которым следует двоеточие и тело функции:

name: "Hello, ${name}"

Функцию можно тут же вызвать с помощью:

(x: x + 1) 100
# 101

Несколько аргументов (каррирование)

Технически любая функция Nix может принимать только один аргумент. Но иногда функция требует нескольких - и это достигается через каррирование:

name: age: "${name} is ${toString age} years old"

На самом деле это не одна функция, а две. Первая принимает age и подставляет в строку, а вторая принимате name и подставляет в строку, которую вернула первая функция.

Еще одно преимущество таких функций - возможность передать один параметр и получить функцию, которую можно записать в переменную, то есть частично применить:

let
  multiply = a: b: a * b;
  doubleIt = multiply 2;
in doubleIt 15
# 30

Несколько аргументов (наборы атрибутов)

Другой способ указать несколько аргументов - передать набор атрибутов, который включает в себя все аргументы:

{ name, age }: "${name} is ${toString age} years old"

Используя этот метод, мы можем задать значения по умолчанию:

let
  greeter =  { name, age ? 42 }: "${name} is ${toString age} years old";
in greeter { name = "Slartibartfast"; }

Помимо этого, если необходимо дать функции возможность принимать неограниченное количество аргументов, можно воспользоваться ...:

let
  greeter = { name, age, ... }: "${name} is ${toString age} years old";
  person = {
    name = "Slartibartfast";
    age = 42;
    # Атрибут email необязателен, но ...
    email = "slartibartfast@magrath.ea";
  };
in greeter person # ... ошибки не возникает благодаря этой конструкции

Также можно назвать весь набор каким нибудь именем с помощью такого синтаксиса:

let
  func = { name, age, ... }@args: builtins.attrNames args;
in func {
  name = "Slartibartfast";
  age = 42;
  email = "slartibartfast@magrath.ea";
}

Условная конструкция

Nix имеет простую поддержку условных конструкций. Не забудьте что if - тоже выражение, следовательно наличие then и else обязательно.

if someCondition
then "it was true"
else "it was false"

Проверка assert

Для проверки утверждений в Nix есть специальная конструкция assert:

assert 1 + 1 = 2;
"yes!"

Ключевое слово inherit

Это очень полезное и простое ключевое слово, которое используется для привязки переменной из родительской области видимости. Проще говоря, inherit foo; это то же самое, что foo = foo;.

Помимо этого inherit поддерживает привязку сразу нескольких переменных, а также привязку переменных из наборов атрибутов:

{
  inherit name age; # Тоже, что и name = name; age = age;
  inherit (otherAttrs) email; # То же что и email = otherAttrs.email;
}

Оператор with

Этот оператор крайне полезен. Он импортирует все атрибуты из набора в переменные с соответствующими именами:

let
  attrs = {
    a = 15;
    b = 2;
  };
in with attrs; a + b

Импорты

Файлы Nix могут импортировать друг друга с помощью встроенной функции import:

let
  myLib = import ./lib.nix;
in myLib.usefulFunction 42;

Функция import оценивает файл и возвращает его Nix значение. Часто файлы Nix начинаются с заголовка функции для передачи параметров в остальную часть файла, поэтому вы часто можете видеть импорт в формате import ./lib.nix { ... };.

Кстати, у Nix есть интересная переменная окружения NIX_PATH, которая содержит псевдонимы для путей к файлам, содержащих выражения Nix. По умолчанию в ней присутствует несколько каналов (nixpkgs и возможно nixos-unstable). К ним можно получить доступ с помощью следующего синтаксиса:

let
  pkgs = import <nixpkgs> { };
in pkg.something

Функция map

Эта функция применяет переданную ей функцию к каждому элементу переданного ей списка:

map (x: x + x) [ 1 2 3 ]
# [ 2 4 6 ]

Выражение or

В Nix есть ключевое слово or, которое может использоваться для безопасного доступа к атрибуту. Если он не установлен, будет возвращено значение по умолчанию:

let
  set1 = { a = 42; };
  set2 = { };
in set1.a or 23 + set2.a or 23 # 65

Стандартные библиотеки

В Nix существует 3 стандартных библиотеки, и желательно знать все три.

builtins

Nix поставляется с несколькими встроенными функциями, они работают независимо от чего-либо. Большинство из них реализованы в самом интерпретаторе Nix, так что они достаточно быстры по сравнению с функциями, написанными на Nix.

В руководстве Nix есть раздел, в котором перечислены все builtins и их использование.

Вот одни из самых часто используемых функций отсюда:

  • derivation - стандартная деривация (см. Деривации)
  • toJSON / fromJSON - перевод аттерсетов/списков в JSON формат и обратно
  • toString - преобразование в строку
  • toPath / fromPath - перевод строк в пути и обратно

Здесь также есть несколько функций, ломающих чистоту вычислений Nix:

  • fetchGit - скачивает Git репозиторий, по умолчанию используя конфигурацию git/ssh
  • fetchTarball - скачивает и извлекает архивы без указания хешей

pkgs.lib

Nixpkgs помимо обычных пакетов также содержит дочерний набор атрибуто lib, который содержит огромное количество полезных функций. Полный список с документацией по каждой вы можете найти здесь.

pkgs сам по себе

Сам Nixpkgs помимо пакетов и lib аттерсета содержит функции, с которыми вы можете столкнуться при создании новых пакетов Nix.

К сожалению, поисковиков именно по пакетам из pkgs нет. Вы может найти их на noogle.dev, в Nixpkgs Reference Manual, но чистого списка функций не будет. Поэтому я написал специально для вас небольшую программу, которая ищет все функции в pkgs:

let
  pkgs = import <nixpkgs> { };

  tryEval =
    expr:
    let
      result = builtins.tryEval expr;
    in
    if result.success then result.value else null;

  safeGet = name: tryEval (builtins.getAttr name pkgs);
  isFunction = x: builtins.isFunction x;
  isTopLevelFunction = name: name != "lib" && isFunction (safeGet name);
  functionNames = builtins.filter isTopLevelFunction (builtins.attrNames pkgs);
in
builtins.trace (builtins.concatStringsSep "\n" functionNames) { }

Для запуска создайте Nix файл с любым названием и запустите с помощью nix eval -f <filename>.nix.

Деривации

При оценке Nix выражения вы можете получить одну и более дериваций. Они описывают действия для сборки, которые при запуске помещают выходные данные в /nix/store.

Встроенная функция derivation отвечает за создание низкоуровненвых производных. Обычно при опакечивании программ используются более высокоуровненвые, такие как stdenv.mkDerivation.

Деривации - отдельная большая тема, так что про нее я расскажу в другой статье.