В статье прошлой недели мы обсудили основы менеджера пакетов Nix. Nix привносит некоторые идеи функциональной чистоты в управление пакетами в Haskell. Например, когда вы загружаете пакет, нет никаких побочных эффектов для других пакетов в вашей системе. И любой пакет является прямым продуктом его зависимостей и исходного кода, генерирующим уникальный хэш.

Но Nix обычно не работает автономно с проектами Haskell. Как и Stack, он фактически интегрируется с Cabal под капотом. Мы продолжим использовать файл .cabal для описания нашего пакета. Но аккуратный инструмент позволяет получить преимущества Nix в связке с Cabal. На этой неделе мы рассмотрим, как сделать базовый проект с этой комбинацией.

Если вы новичок в Haskell, вам, вероятно, лучше начать со Stack, а не с Nix. Прочтите нашу серию Лифтофф, чтобы ознакомиться с основами языка. Тогда вы можете пройти наш бесплатный мини-курс по стеку, чтобы познакомиться со стеком.

Инициализация нашего проекта с помощью Nix

На прошлой неделе мы исследовали команду nix-shell. Это позволяет вам запускать команды, как если бы у вас были установлены определенные пакеты. Таким образом, даже если в нашей системе не установлены GHC или Cabal, мы все равно можем запустить следующую команду:

>> nix-shell --packages ghc cabal-install --run "cabal init"

Это откроет оболочку, в которой Никс предоставит нам необходимые пакеты. Затем он запускает пакет cabal init, как мы видели в нашем руководстве по Cabal несколько недель назад. Это дает нам базовый проект Cabal, который мы можем построить и запустить с помощью инструментов Cabal.

Преобразование в Никс

Но вместо этого мы собираемся преобразовать его в проект Nix! Есть отличный ресурс, который делает это преобразование для нас. Программа cabal2nix может взять файл Cabal и преобразовать его в файл пакета nix. Как нам установить эту программу? Ну, с помощью Nix, конечно! Мы можем даже пропустить его установку и запустить команду через nix-shell. Обратите внимание, как мы передаем текущий каталог '.' в качестве входных данных для команды и направить вывод в наш nix-файл default.nix.

>> nix-shell --packages cabal2nix --run "cabal2nix ." > default.nix

Вот результирующий файл:

{ mkDerivation, base, stdenv }:
mkDerivation {
 pname = "MyNixProject";
 version = "0.1.0.0";
 src = ./.;
 isLibrary = false;
 isExecutable = true;
 executableHaskellDepends = [ base ];
 license = "unknown"
 hydraPlatforms = stdenv.lib.platforms.none;
}

Этот файл содержит одно выражение на языке Nix. Первая строка содержит три «входа» нашего выражения. Из них base — наша единственная зависимость от Haskell. Затем mkDerivation — это функция Nix, которую мы можем использовать для создания нашего выражения вывода. Наконец, есть эта зависимость stdenv. Это специальный ввод, сообщающий Nix, что у нас стандартная среда Linux. Nix предполагает, что у него есть определенные вещи, такие как GCC и Bash.

Затем мы назначаем определенные параметры в нашем вызове mkDerivation. Мы предоставляем имя и версию нашего пакета. Мы также заявляем, что он содержит исполняемый файл, а не библиотеку (пока). Есть специальное поле, в котором мы перечисляем зависимости Haskell. На данный момент содержит только base.

Мы пока не можем построить из этого файла. Вместо этого мы создадим другой файл release.nix. Этот файл снова содержит одно выражение Nix. Мы используем импортированную функцию haskellPackages.callPackage для вызова нашего предыдущего файла.

let
 pkgs = import <nixpkgs> { };
in
 pkgs.haskellPackages.callPackage ./default.nix { }

Сейчас мы не предоставляем никаких параметров, поэтому оставим здесь пустые фигурные скобки. Для производственного проекта вы должны использовать этот файл для «привязки» пакетов nix к определенному каналу. Но нам не нужно делать это прямо сейчас.

Теперь мы можем собрать нашу программу, используя команду nix-build и новый файл выпуска:

>> nix-build release.nix

Сборка проекта помещает результаты в каталог result в корне вашего проекта. Итак, мы можем запустить наш простой двоичный файл!

>> ./result/bin/MyNixProject
Hello, Haskell!

Добавление зависимости

Как и в предыдущих уроках, давайте сделаем это немного интереснее, добавив зависимость! Мы снова будем использовать базовый пакет split. Мы создадим функцию lastName в нашей библиотеке следующим образом:

module Lib where
import Data.List.Split (splitOn)
lastName :: String -> String
lastName input = last (splitOn " " input)

Затем мы будем использовать это в нашем исполняемом файле. Вместо того, чтобы просто печатать сообщение, мы получим имя пользователя, а затем напечатаем его фамилию:

module Main where
import Lib (lastName)

main :: IO ()
main = do
 putStrLn "What's your name?"
 input <- getLine
 let lName = lastName input
 putStr "Your last name is: "
 putStrLn lName

Нам нужно обновить наш файл .cabal по сравнению с его начальным состоянием, чтобы включить библиотеку и новую зависимость. Обратите внимание, что мы изменили имя исполняемого файла, чтобы избежать путаницы с библиотекой.

library
 build-depends:
     base >=4.12 && <4.13
   , split == 0.2.3.3
 ...
executable run-nix-project
 build-depends:
     base >=4.12 && <4.13
   , MyNixProject

Теперь, когда мы изменили наш файл .cabal, мы должны снова запустить cabal2nix, чтобы повторно сгенерировать default.nix. Мы делаем это с помощью того же вызова nix-shell, который использовали ранее. (Рекомендуется сохранить эту команду под псевдонимом). Затем мы видим, что default.nix изменилось.

{ mkDerivation, base, split, stdenv }:
mkDerivation {
 pname = "MyNixProject";
 version = "0.1.0.0";
 src = ./.;
 isLibrary = true;
 isExecutable = true;
 libraryHaskellDepends = [base split ];
 executableHaskellDepends = [ base ];
 license = "unknown"
 hydraPlatforms = stdenv.lib.platforms.none;
}

Новый файл отражает изменения в нашем проекте. Пакет split теперь является входом в наш проект. Таким образом, это появляется как «аргумент» в первой строке. Мы добавили библиотеку и видим, что isLibrary изменилось на true. Также есть новая строка для libraryHaskellDepends. Он содержит пакет split, который мы используем в качестве зависимости. Теперь Nix возьмет на себя задачу найти нужную зависимость на нашем канале!

Мы снова можем собрать наш код, используя nix. Мы увидим другое поведение, теперь под другим именем исполняемого файла!

>> nix-build release.nix
>> ./result/bin/run-nix-project
What's your name?
John Test
Your last name is: Test

Вывод

На этом мы завершаем наш первый взгляд на создание проекта Haskell с помощью Nix. У нас все еще есть Cabal, предоставляющий базовое описание нашего пакета. Но на самом деле Nix предоставляет нам базу данных пакетов. На следующей неделе мы углубимся в эти детали еще немного и сделаем еще один шаг вперед. Мы покажем, как мы можем использовать Stack и Nix вместе, чтобы объединить все различные менеджеры пакетов, которые мы рассмотрели.

Как я уже упоминал ранее, у Nix более крутая кривая обучения, чем у других наших инструментов. В частности, не так много понятных руководств. Поэтому я рекомендую начать со Stack, прежде чем изучать Nix. Вы можете сделать это, пройдя наш Мини-курс по стеку.

Учитывая отсутствие четких руководств, я хочу выделить пару ресурсов, которые помогли мне написать эту статью. Во-первых, репозиторий haskell-nix на Github, написанный Габриэлем Гонсалесом. Второй этот пост в блоге Соареса Чена. Это хорошие ресурсы, если вы хотите немного продвинуться в Haskell и Nix.