Comment utiliser Nix pour développer en Haskell

Voir aussi : video youtube - code source

Nix est un gestionnaire de paquets et d’environnements logiciels qui est assez pratique pour développer en Haskell. Cet article en présente quelques fonctionnalités de base.

Exemple de base

On veut développer ou exécuter le code suivant :

import Data.Char (toUpper)
import Data.Function ((&))
import qualified Streamly.Internal.FileSystem.File as File
import qualified Streamly.Prelude as Stream
import qualified Streamly.Data.Unicode.Stream as Stream
import System.Environment (getArgs)

main :: IO ()
main = do
    args <- getArgs
    case args of
        [filename] -> 
              Stream.unfold File.read filename
            & Stream.decodeUtf8
            & Stream.map toUpper
            & Stream.mapM_ putChar
        _ -> putStrLn "usage: <filename>"

Ce code dépend de la bibliothèque streamly. Comme cette dernière n’est généralement pas installée de base, l’exécution de notre code échouera probablement :

$ runghc Main.hs /etc/hostname

Main.hs:3:1: error:
    Could not find module ‘Streamly.Internal.FileSystem.File’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
3 | import qualified Streamly.Internal.FileSystem.File as File
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...

Nix permet de définir des environnements logiciels pour résoudre ce problème de dépendances et à différents niveaux (système global, utilisateur, projet).

Script dans un nix-shell

Pour définir l’environnement de notre code d’exemple, on peut écrire le fichier shell.nix suivant :

let

  pkgs = import <nixpkgs> {};

  ghc = pkgs.haskellPackages.ghcWithPackages (ps: with ps; [
    streamly
  ]);

in pkgs.stdenv.mkDerivation {
  name = "haskell-env";
  buildInputs = [ ghc ];
  shellHook = "eval $(egrep ^export ${ghc}/bin/ghc)";
}

On peut ensuite charger cet environnement avec la commande nix-shell, ce qui permettra d’avoir toutes dépendances nécessaires et donc d’exécuter notre code :

$ nix-shell
...

[nix-shell]$ runghc Main.hs /etc/hostname 
NIXOS

[nix-shell]$ exit

$

Le fichier shell.nix permet de faire beaucoup d’autres choses (voir la doc de haskell4nix), par exemple préciser la version du compilateur ou de la logithèque à utiliser, ajouter ou modifier des bibliothèques Haskell, inclure des dépendances générales (non spécifiques à Haskell), etc.

Script autonome

Pour éviter d’écrire le fichier shell.nix et d’avoir à lancer un nix-shell, on peut spécifier l’interpréteur et les options à utiliser directement dans le fichier de code :

#!/usr/bin/env nix-shell
#!nix-shell -i "runghc" -p "haskellPackages.ghcWithPackages (pkgs: [pkgs.streamly])"

import Data.Char (toUpper)
...

Ainsi, il suffit d’ajouter les droits d’exécution au fichier et on peut alors le lancer directement :

$ ./Main.hs /etc/hostname 
NIXOS

Cependant, cette méthode est rarement une bonne idée car elle rend le code dépendant de Nix et n’est pas performante à l’exécution du fait de runghc.

Projet cabal

Pour un vrai projet en Haskell, on utilise plutôt un outil de configuration de projet comme cabal. Pour cela, on écrit un fichier de configuration, comme ici upperize.cabal, indiquant notamment les dépendances et les fichiers de code :

cabal-version:      2.2
name:               upperize
version:            1.0
build-type:         Simple
license:            MIT

common deps
    ghc-options:        -Wall -O
    default-language:   Haskell2010
    build-depends:      base, streamly

executable upperize
    import:             deps
    hs-source-dirs:     app
    main-is:            Main.hs

Nix permet alors d’utiliser ce fichier upperize.cabal automatiquement. Pour cela, il suffit d’écrire le fichier default.nix suivant :

{ pkgs ? import <nixpkgs> {} }:
let drv = pkgs.haskellPackages.callCabal2nix "upperize" ./. {};
in if pkgs.lib.inNixShell then drv.env else drv

On peut ensuite ouvrir un nix-shell et lancer les commandes cabal :

$ nix-shell
...

[nix-shell]$ cabal run upperize /etc/hostname 
...

NIXOS

Nix peut également utiliser cette configuration pour réutiliser le code dans un autre projet, construire une image docker, etc. Par exemple, pour installer le programme sur le système :

$ nix-env -if . 
installing 'upperize-1.0'
...

$ upperize /etc/hostname 
NIXOS

Conclusion

Nix est assez pratique pour développer en Haskell. Pour des petits codes, il permet de créer simplement des environnements logiciels satisfaisants. Pour des gros projets, il permet de réutiliser automatiquement la configuration Haskell. Nix permet également de créer des envrionnements plus adaptés au développement, notamment avec des outils de documentation ou d’analyse de code. Enfin, Nix propose également un nouveau système de configuration intéressant : Flakes.