Введение
Чтобы попробовать примеры кода отсюда, вам необходим установленный Nix. Введите в консоли nix repl и пробуйте!
Если вы хотите запускать фалы с кодом, вам пригодится команда nix-instantiate --eval file.nix.
В некоторых случаях такой запуск файла может дать вывод наподбие { a = <CODE>; }. Если это происходит, перед флагом --eval добавьте флаг --strict.
Nix - чисто функциональный язык программирования. В нем нет понятия последовательного выполнения шагов - любые зависимости от операций выполняются только через данные, полученные от других операций. Говоря простым языком, порядок не важен. Например:
Здесь a и b - просто выражения. Nix смотрит: нам нужен b. Чтобы вычислить его, нужно знать a. Он сам понимает это и вычисляет только то, что нужно и только тогда, когда нужно.
А вот в императивных языках программирования все по-другому: ты пишешь инструкции одну за другой, и они выполняются по порядку. Рассмотрим пример на Golang:
Сначала выполнится первая строка, затем вторая, и так далее. Порядок важен.
Разберем пример потруднее. Здесь используется функция builtins.throw - вызывает ошибку, если до нее дойдет вычисление.
attrs - набор атрибутов (что то вроде словаря или объекта), у него есть два поля - a и b. Но в основной части с in вы используем только a, поэтому "ошибочное" значение b не вычисляется.
Если мы добавим b в вычисляемую область, мы увидим вывод error: Оу нет!.
Помимо этого, любой фрагмент Nix кода является выражением, возращающим значение. Оценка (вычисление) выражения дает одну структуру данных, а не последовательность операций. Каждый файл Nix вычисляется в одно выражение.
Также Nix является "ленивым" языком. Мы уже рассмотрели выше пример с throw, когда он не вычисляется, ведь не используется в программе. Это и называется ленивостью.
Nix создан для конкретной цели - для взаимодействия с менеджером пакетов Nix. Хотя иногда его используют для других задач, он не является языком общего назначения.
Языковые конструкции
Здесь я расскажу о конструкциях в Nix. Это небольшой язык, и большинство из них очевидны.
Основные типы данных
Операторы
| Оператор | Описание |
|---|---|
+, -, *, / | Стандартные арифметические операции |
+ | Конкатенация строк |
++ | Объединение списков |
==, != | Логическое равенство/неравенство |
>, >=, <, <= | Логические операции сравнения |
&&, || | Логические AND и OR |
-> | Логическая импликация |
! | Отрицание |
set.attr | Доступ к атрибуту attr из набора set |
set ? attribute | Проверяет, содержит ли набор атрибут |
left // right | Объединяет два набора атрибутов |
Оператор объединения //
Этот оператор широко используется в Nix коде, поэтому вам желательно изучить его. В других языках таких операторов почти не бывает.
Он объединяет два набора атрибутов, переданных ему:
Значение справа от оператора имеют больший приоритет:
Этот оператор не умеет рекурсивно объединять списки:
Вместо этого используйте lib.mkMerge:
Переменные
В Nix переменные вводятся с помощью выражения let ... in. Они неизменяемы и доступны только в области действия этого выражения. Глобальных переменных нет.
Функции
Все функции в Nix являются лямбда-функциями. Это означает, что они обрабатываются как данные. Давать им имена можно с помощью присваивания их переменным или устанавливая их в качестве значений набора атрибутов.
Объявление функции - это просто единственный аргумент, за которым следует двоеточие и тело функции:
Функцию можно тут же вызвать с помощью:
Несколько аргументов (каррирование)
Технически любая функция Nix может принимать только один аргумент. Но иногда функция требует нескольких - и это достигается через каррирование:
На самом деле это не одна функция, а две. Первая принимает age и подставляет в строку, а вторая принимате name и подставляет в строку, которую вернула первая функция.
Еще одно преимущество таких функций - возможность передать один параметр и получить функцию, которую можно записать в переменную, то есть частично применить:
Несколько аргументов (наборы атрибутов)
Другой способ указать несколько аргументов - передать набор атрибутов, который включает в себя все аргументы:
Используя этот метод, мы можем задать значения по умолчанию:
Помимо этого, если необходимо дать функции возможность принимать неограниченное количество аргументов, можно воспользоваться ...:
Также можно назвать весь набор каким нибудь именем с помощью такого синтаксиса:
Условная конструкция
Nix имеет простую поддержку условных конструкций. Не забудьте что if - тоже выражение, следовательно наличие then и else обязательно.
Проверка assert
Для проверки утверждений в Nix есть специальная конструкция assert:
Ключевое слово inherit
Это очень полезное и простое ключевое слово, которое используется для привязки переменной из родительской области видимости. Проще говоря, inherit foo; это то же самое, что foo = foo;.
Помимо этого inherit поддерживает привязку сразу нескольких переменных, а также привязку переменных из наборов атрибутов:
Оператор with
Этот оператор крайне полезен. Он импортирует все атрибуты из набора в переменные с соответствующими именами:
Импорты
Файлы Nix могут импортировать друг друга с помощью встроенной функции import:
Функция import оценивает файл и возвращает его Nix значение. Часто файлы Nix начинаются с заголовка функции для передачи параметров в остальную часть файла, поэтому вы часто можете видеть импорт в формате import ./lib.nix { ... };.
Кстати, у Nix есть интересная переменная окружения NIX_PATH, которая содержит псевдонимы для путей к файлам, содержащих выражения Nix. По умолчанию в ней присутствует несколько каналов (nixpkgs и возможно nixos-unstable). К ним можно получить доступ с помощью следующего синтаксиса:
Функция map
Эта функция применяет переданную ей функцию к каждому элементу переданного ей списка:
Выражение or
В Nix есть ключевое слово or, которое может использоваться для безопасного доступа к атрибуту. Если он не установлен, будет возвращено значение по умолчанию:
Стандартные библиотеки
В Nix существует 3 стандартных библиотеки, и желательно знать все три.
builtins
Nix поставляется с несколькими встроенными функциями, они работают независимо от чего-либо. Большинство из них реализованы в самом интерпретаторе Nix, так что они достаточно быстры по сравнению с функциями, написанными на Nix.
В руководстве Nix есть раздел, в котором перечислены все builtins и их использование.
Вот одни из самых часто используемых функций отсюда:
derivation- стандартная деривация (см. Деривации)toJSON/fromJSON- перевод аттерсетов/списков в JSON формат и обратноtoString- преобразование в строкуtoPath/fromPath- перевод строк в пути и обратно
Здесь также есть несколько функций, ломающих чистоту вычислений Nix:
fetchGit- скачивает Git репозиторий, по умолчанию используя конфигурацию git/sshfetchTarball- скачивает и извлекает архивы без указания хешей
pkgs.lib
Nixpkgs помимо обычных пакетов также содержит дочерний набор атрибуто lib, который содержит огромное количество полезных функций. Полный список с документацией по каждой вы можете найти здесь.
pkgs сам по себе
Сам Nixpkgs помимо пакетов и lib аттерсета содержит функции, с которыми вы можете столкнуться при создании новых пакетов Nix.
К сожалению, поисковиков именно по пакетам из pkgs нет. Вы может найти их на noogle.dev, в Nixpkgs Reference Manual, но чистого списка функций не будет. Поэтому я написал специально для вас небольшую программу, которая ищет все функции в pkgs:
Для запуска создайте Nix файл с любым названием и запустите с помощью nix eval -f <filename>.nix.
Деривации
При оценке Nix выражения вы можете получить одну и более дериваций. Они описывают действия для сборки, которые при запуске помещают выходные данные в /nix/store.
Встроенная функция derivation отвечает за создание низкоуровненвых производных. Обычно при опакечивании программ используются более высокоуровненвые, такие как stdenv.mkDerivation.
Деривации - отдельная большая тема, так что про нее я расскажу в другой статье.