Функции и импорт

Добро пожаловать в пятую пилюлю 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"; }.

Когда сообщение будет напечатано? Тогда, когда вычисления доберутся до соответствующей ветви кода.

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

…мы, наконец, напишем свою первую деривацию.