Основы языка
Добро пожаловать на четвёртую пилюлю Nix. В предыдущей статье мы познакомились с окружениями Nix. Мы устанавливали программы, управляли профилем, переключались между поколениями и писали запросы к хранилищу Nix. Всё это — основы системного администрирования с помощью Nix.
Для написания выражений, которые конструируют деривации, используется Язык Nix. Для построения дериваций из выражений используется утилита nix-build. Даже если вы системный администратор (а не программист), вам нужно освоить Nix, если вы хотите настраивать систему. Используя Nix в вашей работе, в качестве бонуса вы получаете все те возможности, о которых я рассказывал в прошлых статьях.
Синтаксис Nix достаточно непривычный, так что, разбираясь с примерами, вы можете решить, что здесь замешана какая-то магия. На самом деле речь в основном идёт об удобном написании служебных функций.
Этот синтаксис прекрасно подходит для описания пакетов, так что изучение языка окупится при написании пакетных выражений.
📢 В Nix всё является выражением, там нет операторов. Обычное дело в функциональных языках.
📢 Значения в Nix неизменяемые (иммутабельные).
Типы значений
В Nix 2.0 есть команда nix repl
.
Это простая утилита, которая позволяет экспериментировать с языком Nix.
Напомню, что Nix — это не только набор утилит для работы с деривациями, но и чистый ленивый функциональный язык.
Синтаксис nix repl
немного отличается от синтаксиса Nix, когда речь заходит о присваивании переменных (ведь в функциональных языках не бывает присваиваний).
Просто помните об этом, и вы не запутаетесь.
Эксперименты с nix repl
помогут нам быстрее вкатиться в язык.
Запустите nix repl
. Прежде всего, Nix поддерживает основные арифметические операции: +
, -
, *
и /
.
(Чтобы выйти из nix repl
, введите команду :q
.
Команда :?
выводит справку.)
nix-repl> 1+3
4
nix-repl> 7-4
3
nix-repl> 3*2
6
Попытка выполнить деление в Nix может вас удивить.
nix-repl> 6/3
/home/nix/6/3
Что произошло?
Вспомним, что Nix не является языком общего назначения, это предметно-ориентированный язык для написания пакетов.
Деление чисел — не самая нужная операция при написании пакетных выражений.
Для Nix 6/3
— это путь, построенный относительно текущего каталога.
Чтобы заставить Nix выполнить деление, добавьте пробел после /
, либо вызовите встроенную функцию builtins.div
.
nix-repl> 6/ 3
2
nix-repl> builtins.div 6 3
2
Другие операторы — это ||
, &&
и |
для булевых значений и операторы сравнения, такие как !=
, ==
, <
, >
, <=
, >=
.
В Nix <
, >
, <=
and >=
используются нечасто.
Есть и другие операторы, с которыми мы познакомимся в этом цикле статей.
В Nix есть простые типы: целые числа, числа с плавающей запятой, строки, пути, булевы значения и null. Кроме того, есть списки, множества и функции. Этих типов хватает, чтобы собрать целую операционную систему.
Nix является сильно типизированным, но не статически типизированным языком. То есть, вы не можете смешивать строки и целые числа без предварительного преобразования типов.
Мы выяснили, что выражения считаются путями, если после символа деления нет пробела.
Поэтому, чтобы указать текущий каталог, пишите ./.
Кроме того, Nix умеет распознавать url’ы.
Не все url’ы или пути могут быть распознаны обычным образом. Если возникает ошибка распознавания, вы всегда можете вернуться к обычным строкам. Строковые url’ы и пути также обеспечивают дополнительную безопасность.
Идентификаторы
Идентификаторы в Nix такие же, как в других языках, за исключением того, что позволяют писать дефис (-
).
Удобно, имея дело с пакетами, писать дефис в имени.
Пример:
nix-repl> a-b
error: undefined variable `a-b' at (string):1:1
nix-repl> a - b
error: undefined variable `a' at (string):1:1
Как видите, a-b
распознаётся как идентификатор, а не как вычитание.
Строки
Строки заключаются в двойные кавычки ("
) или в пару одинарных кавычек (''
).
nix-repl> "foo"
"foo"
nix-repl> ''foo''
"foo"
В других языках, например, в Python, можно заключать строки в одиночные кавычки ('foo'
), но не в Nix.
Можно интерполировать выражения Nix внутри строк с помощью синтаксиса ${...}
. Если вы писали на других языках, то можете по привычке написать $foo
или {$foo}
, но этот синтаксис работать не будет.
nix-repl> foo = "strval"
nix-repl> "$foo"
"$foo"
nix-repl> "${foo}"
"strval"
nix-repl> "${2+3}"
error: cannot coerce an integer to a string, at (string):1:2
Помните, что присваивание foo = "strval"
— это специальный синтаксис, доступный только в nix repl
и недоступный в обычном языке.
Как я уже говорил, нельзя смешивать целые числа и строки, нужно в явном виде приводить тип. Мы вернёмся к обсуждению этого вопроса позже, как и к вызову функций.
Заключая строку в пару одинарных кавычек, можно писать двойные кавычки внутри без необходимости их экранировать.
nix-repl> ''test " test''
"test \" test"
nix-repl> ''${foo}''
"strval"
Экранирование ${...}
в строках с двойными кавычками делается с помощью обратной косой линии (бекслеша), а в строках с парой одиночных кавычек — с помощью ''
:
nix-repl> "\${foo}"
"${foo}"
nix-repl> ''test ''${foo} test''
"test ${foo} test"
Списки
Списки — это последовательность выражений, разделённая пробелами (не запятыми):
nix-repl> [ 2 "foo" true (2+3) ]
[ 2 "foo" true 5 ]
Списки, как и всё в Nix, неизменяемы (иммутабельны). Добавление или удаление элементов в списке возможно, но возвращает новый список.
Наборы атрибутов
Набор1 атрибутов — это ассоциативный массив со строковыми ключами и значениями Nix. Ключи могут быть только строками. Если ключи являются правильными идентификаторами, их можно записывать без кавычек.
nix-repl> s = { foo = "bar"; a-b = "baz"; "123" = "num"; }
nix-repl> s
{ "123" = "num"; a-b = "baz"; foo = "bar"; }
Набор атрибутов можно перепутать с набором аргументов при вызове функций, но это разные вещи.
Чтобы обратиться к элементу в наборе атрибутов:
nix-repl> s.a-b
"baz"
nix-repl> s."123"
"num"
Чтобы обратиться к ключу, который не является правильным идентификатором, используйте кавычки.
Внутри набора нельзя ссылаться на другие элементы или на сам набор:
nix-repl> { a = 3; b = a+4; }
error: undefined variable `a' at (string):1:10
Это можно делать с помощью рекурсивных наборов:
nix-repl> rec { a = 3; b = a+4; }
{ a = 3; b = 7; }
Такая возможность полезна при описании пакетов, которые часто имеют рекурсивную природу.
Выражения ‘if’
Это всё ещё выражения, не операторы.
nix-repl> a = 3
nix-repl> b = 4
nix-repl> if a > b then "yes" else "no"
"no"
Нельзя записывать только ветку then
без ветки else
, потому что у выражения при любом раскладе должен быть результат.
Выражения ‘let’
Выражения ‘let’ используются, чтобы определить локальные переменные для других (внутренних) выражений.
nix-repl> let a = "foo"; in a
"foo"
Синтаксис такой: сначала определяем переменные, затем пишем ключевое слово in
, затем выражение, в котором можно ссылаться на определённые переменные.
Значением всего выражения let
будет значение выражения после in
.
nix-repl> let a = 3; b = 4; in a + b
7
Попробуем записать два выражения let
, одно внутри другого:
nix-repl> let a = 3; in let b = 4; in a + b
7
Помните, что с помощью let
нельзя присвоить переменной другое значение.
Однако, можно перекрывать внешние переменные:
nix-repl> let a = 3; a = 8; in a
error: attribute `a' at (string):1:12 already defined at (string):1:5
nix-repl> let a = 3; in let a = 8; in a
8
Нельзя ссылаться на переменные в выражении let
снаружи:
nix-repl> let a = (let c = 3; in c); in c
error: undefined variable `c' at (string):1:31
Можно ссылаться на переменные в выражении let
, определяя другие перменные, как в рекурсивных наборах.
nix-repl> let a = 4; b = a + 5; in b
9
Общее правило: избегайте ситуаций, когда вам надо сослаться на внешнюю переменную, но переменная с таким же именем есть в текущем выражении let
.
Это же правило действует и в отношении рекурсивных наборов.
Выражения ‘with’
Это непривычный тип выражений — его нечасто можно встретить в других языках.
Можно считать его расширенной версией оператора using
из C++, или from module import*
из Python.
Конструкция with
включает атрибуты набора в область видимости.
nix-repl> longName = { a = 3; b = 4; }
nix-repl> longName.a + longName.b
7
nix-repl> with longName; a + b
7
Оператор получает набор атрибутов и включает их в область видимости вложенного выражения. Естественно, в область видимости попадают только корректные иднетификаторы. Переменные из внешней области видимости с совпадающими именами не перекрываются. В случае необходимости вы всегда можете обратиться к атрибуту через набор:
nix-repl> let a = 10; in with longName; a + b
14
nix-repl> let a = 10; in with longName; longName.a + b
7
Ленивые вычисления
Nix вычисляет выражения только тогда, когда ему нужен результат. Эта особенность языка активно используется при описании пакетов.
nix-repl> let a = builtins.div 4 0; b = 6; in b
6
Здесь значение a
не требуется, поэтому ошибка деления на ноль не возникает — выражение просто не вычисляется.
Из-за этой особенности языка, пакеты можно определять по мере необходимости, при этом доступ к ним осуществляется очень быстро.
В следующей пилюле
…поговорим о функциях и импорте. В этой пилюле я старался избегать функций, иначе пост стал бы слишком большим.
Оригинальный термин set обычно переводится на русский, как множество. Но в нашем случае термин множество вводит в заблуждение, поскольку set содержит не отдельные элементы, а пары ключ-значение. Такую стркутуру обычно называют называют словарём или ассоциативным массивом. В конечном итоге я остановился на слове набор, которое всё-таки не совсем множество.