Основы языка

Добро пожаловать на четвёртую пилюлю 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 не требуется, поэтому ошибка деления на ноль не возникает — выражение просто не вычисляется. Из-за этой особенности языка, пакеты можно определять по мере необходимости, при этом доступ к ним осуществляется очень быстро.

В следующей пилюле

…поговорим о функциях и импорте. В этой пилюле я старался избегать функций, иначе пост стал бы слишком большим.

1

Оригинальный термин set обычно переводится на русский, как множество. Но в нашем случае термин множество вводит в заблуждение, поскольку set содержит не отдельные элементы, а пары ключ-значение. Такую стркутуру обычно называют называют словарём или ассоциативным массивом. В конечном итоге я остановился на слове набор, которое всё-таки не совсем множество.