Функции и импорт
Добро пожаловать в пятую пилюлю Nix.
В предыдущей четвёртой пилюле мы начали изучение языка программирования Nix: рассказали про основные типы и значения языка, и про базовые выражения, такие как if
, with
и let
.
Чтобы закрепить материал, запустите сессию REPL и поэкспериментируйте с выражениями Nix.
Функции часто используются, чтобы строить повторно используемые компоненты в больших хранилищах, скажем, в nixpkgs. В руководстве по языку Nix есть великолепное объяснение функций, так что я буду часто на него ссылаться.
Напоминаю, как запустить среду 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
Объяснение:
- В
test.nix
мы возвращаем функцию. Она принимет набор, где у атрибутовb
,trueMsg
иfalseMsg
есть значения по умолчанию. builtins.trace
— встроенная функция, которая принимает два аргумента. Первый — это сообщение для печати, второй — возвращаемое значение. Обычно она используется для отладки.- В конце мы импортируем
test.nix
и вызываем функцию с набором{ a = 5; trueMsg = "ok"; }
.
Когда сообщение будет напечатано? Тогда, когда вычисления доберутся до соответствующей ветви кода.
В следующей пилюле
…мы, наконец, напишем свою первую деривацию.