Добро пожаловать на пятую пилюлю Nix. В предыдущей четвёртой пилюле мы начали изучения языка программирования Nix. Мы рассказали про основные типы и значения языка, и про базовые выражения, такие как if
, with
и let
. Чтобы закрепить материал, запустите сессию REPL и поэкспериментируйте с выражениями Nix.
Функции часть используются, чтобы строить повторно используемые компоненты в больших хранилищах, скажем, в nixpkgs. В руководстве по языку Nix есть [великолепное объяснение функций] (https://nixos.org/manual/nix/stable/expressions/language-constructs.html#functions), так что я буду часто на него ссылаться.
Напоминаю, как запустить среду Nix: source ~/.nix-profile/etc/profile.d/nix.sh
Безымянные с единственным параметром
В Nix функции всегда анонимы (то есть являются лямбдами) и у них всегда один параметр. Синтаксис экстремально прост: пишите имя параметра, затем ":
", затем тело функции.
nix-repl> x: x*2
«lambda»
Здесь мы определили функцию, которая принимает параметр x
и возвращает x*2
. Проблема в том, что мы не можем её вызвать, потому что у неё нет имени... шутка!
Мы можем дать функции имя, связав её с переменной.
nix-repl> double = x: x*2
nix-repl> double
«lambda»
nix-repl> double 3
6
Как я писал ранее, присваивание существует только в nix repl
, в обычном языке Nix его нет. Итак, мы определили функцию x: x*2
, которая принимает один параметр x
и возвращает x*2
. Затем эта функции присваивается переменной double
. После этого функцию можно вызвать: double 3
.
Важное примечание: в большинстве языков программирования параметры нужно заключать в скобки: double(3)
. В Nix скобки не нужны: double 3
.
Итого: чтобы вызывать функцию, напишите её имя, затем пробел, затем аргумент. Вот так всё просто.
Когда параметров больше одного
Как записать функцию, которая принимает больше одного параметра? Тем, кто не сталкивался с функциональным программированием, потребуется немного времени, чтобы разобраться. Исследуем тему по шагам.
nix-repl> mul = a: (b: a*b)
nix-repl> mul
«lambda»
nix-repl> mul 3
«lambda»
nix-repl> (mul 3) 4
12
Сначала мы определили функцию, которая принимает параметр a
и возвращает другую функцию. Эта другая функция принимает параметр b
и возвращает a*b
. Вызывая mul 3
мы получаем в результате функцию b: 3*b
. Вызывая её с параметром 4
, мы получаем искомый результат.
В этом коде можно вообще отказаться от скобок, поскольку в Nix есть приоритеты операторов:
nix-repl> mul = a: b: a*b
nix-repl> mul
«lambda»
nix-repl> mul 3
«lambda»
nix-repl> mul 3 4
12
nix-repl> mul (6+7) (8+9)
221
Всё выглядит так, как будто у функции mul
два параметра. Из-за того, аргументы разделяются пробелом, нужны скобки, чтобы передавать более сложные выражения. В других языках вы бы написали mul(6+7, 8+9)
.
Поскольку функции имеют только один параметр, несложно использовать частичное применение:
nix-repl> foo = mul 3
nix-repl> foo 4
12
nix-repl> foo 5
15
Мы сохранили функцию, которую вернула mul 3
в переменную foo
, и затем вызывали.
Набор аргументов
Одна из самых мощных возможностей Nix — сопоставление с образцом параметра, который имеет тип набор атрибутов. Напишем альтернативную версию mul = a: b: a*b
сначала используя набор аргументов, а затем — сопоставление с образцом.
nix-repl> mul = s: s.a*s.b
nix-repl> mul { a = 3; b = 4; }
12
nix-repl> mul = { a, b }: a*b
nix-repl> mul { a = 3; b = 4; }
12
В первом случае мы определили функцию, которая принимает один параметр-набор. Затем мы взяли атрибуты a
и b
из этого набора. Заметьте, как элегантно выглядит запись вызова без скобок. В других языках нам пришлось бы написать mul({ a=3; b=4; })
.
Во втором случае мы определили набор аргументов. Это похоже на определение набора атрибутов, только без значений. Мы требуем, чтобы переданный набор содержал ключи a
и b
. Затем мы можем использовать эти a
и b
непосредственно в теле функции.
nix-repl> mul = { a, b }: a*b
nix-repl> mul { a = 3; b = 4; c = 6; }
error: anonymous function at (string):1:2 called with unexpected argument `c', at (string):1:1
nix-repl> mul { a = 3; }
error: anonymous function at (string):1:2 called without required argument `b', at (string):1:1
Функция принимает набор ровно с теми атрибутами, которые были указаны при её определении.
Атрибуты по умолчанию и вариативные атрибуты
В наборе аргументов можно указывать значения атрибутов умолчанию:
nix-repl> mul = { a, b ? 2 }: a*b
nix-repl> mul { a = 3; }
6
nix-repl> mul { a = 3; b = 4; }
12
Функция может принимать больше атрибутов, чем ей нужно. Такие атрибуты называются вариативными:
nix-repl> mul = { a, b, ... }: a*b
nix-repl> mul { a = 3; b = 4; c = 2; }
Здесь вы не можете получить доступ к атрибуту c
. Но вы сможете обратиться к любым атрибутам, дав имя всему набору с помощью @-образца:
nix-repl> mul = s@{ a, b, ... }: a*b*s.c
nix-repl> mul { a = 3; b = 4; c = 2; }
24
Написав name@
перед образцом, вы даёте имя name
всему набору атрибутов.
Преимущества использования наборов аргументов:
Из-за того, что аргументы именованы, вы не должны запоминать их порядок.
В качестве аргументов можно передать набор, что создаёт совершенно новый уровень гибкости и удобства.
Недостатки:
Частичное применение не работает с множествами аргументов. Вы должны определить множество атрибутов целиком, нельзя определить только его часть.
Наборы атрибутов похожи на **kwargs из языка Python.
Импорт
Встроенная в язык функция import
позволяет включать в текст программы другие файлы .nix
. Такой подход весьма распространён в программировании: мы определяем каждый компонент в отдельном файле .nix
, а затем соединяем компоненты, импортируя эти файлы в один модуль.
Начнём с простейшего примера.
a.nix
:
3
b.nix
:
4
mul.nix
:
a: b: a*b
nix-repl> a = import ./a.nix
nix-repl> b = import ./b.nix
nix-repl> mul = import ./mul.nix
nix-repl> mul a b
12
Да, всё действительно настолько просто. Вы импортируете файл, он компилируется в выражение. Важный момент: в импортирующем файле нет доступа к переменным из импортируемого файла.
test.nix
:
x
nix-repl> let x = 5; in import ./test.nix
error: undefined variable `x' at /home/lethal/test.nix:1:1
Чтобы передать информацию в импортируемый модуль, нужно использовать функции. Пример посложнее:
test.nix
:
{ a, b ? 3, trueMsg ? "yes", falseMsg ? "no" }:
if a > b
then builtins.trace trueMsg true
else builtins.trace falseMsg false
nix-repl> import ./test.nix { a = 5; trueMsg = "ok"; }
trace: ok
true
Объяснение:
В
text.nix
мы возвращаем функцию. Она принимает набор, где у атрибутовb
,trueMsg
иfalseMsg
есть значения по умолчанию.builtins.trace
— встроенная функция, которая принимает два аргумента. Первый — это сообщение для печати, второй — возвращаемое значение. Обычно она используется для отладки.В конце мы импортируем
text.nix
и вызываем функцию с набором{ a = 5; trueMsg = "ok"; }
.
Когда сообщение будет напечатано? Тогда, когда вычисления доберутся до соответствующей ветви кода.
В следующей пилюле
...мы, наконец, напишем своё первое порождение.