Notes sur le langage Haskell

Généralités

La suite GHCup fournit l’essentiel de ce qu’il faut pour développer en Haskell, en particulier les outils Stack et Cabal. En prime, on bénéficie du serveur de langage HLS qui fonctionne très bien avec Neovim.1 Outre le compilateur GHC (Glasgow Haskell Compiler), on dispose des programmes runhaskell et runghc pour exécuter des scripts Haskell directement, c’est-à-dire sans passer par une étape de compilation.

Un projet se définit par un ensemble de “packages”, qui sont eux-mêmes composés d’un ensemble de modules qui constituent l’essentiel du code Haskell. Stack et Cabal permettent de gérer la construction et le “packaging” d’un programme Haskell avec ses dépendances. Pour construire un exécutable, il est nécessaire de définir une fonction “main” dans le fichier source, et le nom du module doit être “Main” (ou alors, on ajoute module Main where en tout début de fichier). Le cas échéant, GHC ne produira que des fichiers .o (fichier objet) et .hi (fichier interface, qui est l’équivalent des fichiers d’en-tête .h en langage C).

Exemple de script pour débuter

Voici un exemple simplifié de programme :

-- hello.hs
main = putStrLn "Hello, World!"

On peut le compiler très simplement avec ghc -o hello hello.hs ; on obtient 3 fichiers en sortie, dont un fichier exécutable dont le format dépend du système d’exploitation :

» ls hello*
hello*    hello.hi  hello.hs  hello.o

» ./hello
Hello, World!

On peut améliorer un peu ce programme initial en stockant la chaîne de caractères dans une variable :

-- hello.hs (v2)
msg = "Hello, World!"
main = putStrLn msg

Après compilation, on obtient exactement le même résultat que précédemment. Enfin, on peut créer une fonction simple qui se chargera d’assembler le message de bienvenue :

-- hello.hs (v3)
main :: IO ()
main = putStrLn (f "World")
f x = "Hello, " ++ x ++ "!"

Le serveur HLS devrait surligner que le type de cette fonction f est f :: [Char] -> [Char], ce qui est équivalent à f :: String -> String (une chaîne est une suite ou liste de caractères). Après recompilation, le résultat produit est identique. Comme dans les exemples précédents, le point d’entrée reste main (comme en C) mais cette fois-ci on spécifie que son type est IO, qui permet de gérer des actions comme dans les langages impératifs.

Exemple de script Haskell sous Neovim (panneau du bas)

Plutôt que de compiler systématiquement des scripts Haskell, il est également possible de spécifier dans une ligne “shebang”

#!/usr/bin/env stack
{- stack runghc -}
main = putStrLn "Hello, World!"

En fonction de la version de stack utilisée (ici, version 3.3.1), GHCup téléchargera et installera automatiquement la version adéquate de GHC (dans ce cas, version 9.10.2, qui correspond à la version LTS Haskell 24.6). Il suffit ensuite de rendre ce fichier exécutable (chmod +x hello.hs sous Linux ou macOS) et de l’éxécuter :

» ./hello.hs
Hello, World!

Notons qu’il est possible de préciser en plus de stack script ou stack runghc le résolveur Haskell à utiliser en dessous de la ligne “shebang”. Enfin, GHCup installe automatiquement runhaskell de sorte que la ligne “shebang” ci-dessus peut-être remplacée par #!/usr/bin/env runhaskell si l’on souhiate travailler avec la version de GHC définit comme courant sous GHCup (e.g., taper ghcup set ghc 9.12.2 dans un terminal pour définir la version par défaut à la dernière en date).

Définition d’un projet avec stack

Jusqu’à présent on s’est contenté d’écrire de simples scripts Haskell et de les compiler ou de les exécuter. Ceci peut s’avérer utile pour de petits programmes, mais dans le cas où le nombre de dépendances augmente il devient préférable d’organiser le programme sous forme d’un ou plusieurs modules au sein d’un projet. Stack facilite la création et la gestion de projets.2

Reprenons l’exemple précédent, en supposant qu’il soit enregistré dans un fichier Hello.hs :

module Main ( main ) where

main :: IO ()
main = putStrLn "Hello, World!"

On ajoute un fichier de description du package, package.yaml, et on enregistre les deux fichiers dans un répertoire “hello” :

name: hello-world
version: 1
dependencies: base
executable:
  main: Hello.hs

Enfin, on ajoute le résolver snapshot: lts-24.0 dans un fichier stack.yaml. Ceci correspond à la version 9.10.2 de GHC. Avec ces deux fichiers de configuration, et le code source Haskell, on dispose d’un projet simplifié mais complètement fonctionnel. La commande stack new va nous permettre d’automatiser toutes ces étapes, comme on le verra plus tard. En attendant, si on lance stack run dans ce répertoire, le fichier source va être compilé et l’exécutable sera lancé en fin de compilation :

» ls
Hello.hs      package.yaml  stack.yaml

» stack run
hello-world> configure (exe)
Configuring hello-world-1...
hello-world> build (exe) with ghc-9.10.2
Preprocessing executable 'hello-world' for hello-world-1...
Building executable 'hello-world' for hello-world-1...
[1 of 2] Compiling Main
[2 of 2] Compiling Paths_hello_world
[3 of 3] Linking .stack-work/dist/aarch64-osx/ghc-9.10.2/build/hello-world/hello-world
ld: warning: -U option is redundant when using -undefined dynamic_lookup
hello-world> copy/register
Installing executable hello-world in /Users/chl/cwd/sandbox/hello/.stack-work/install/aarch64-osx/f46c97d39f43c3a058be73f6626292c876f96e840215d7c2922c09d31bca3a15/9.10.2/bin
Hello, World!

  1. Voici un exemple de configuration à placer dans $HOME/.config/nvim/lsp, et à activer en ajoutant dans $HOME/.config/nvim/init.lua l’instruction vim.lsp.enable({ "hls" }).↩︎

  2. Voir aussi An opinionated guide to Haskell in 2018 de Alexis King.↩︎