Premiers pas avec le langage Haskell
Bonjour le monde!
Un basique [“Hello, World!” program][1] en Haskell peut être exprimé de manière concise en une ou deux lignes :
main :: IO ()
main = putStrLn "Hello, World!"
La première ligne est une annotation de type facultative, indiquant que main
est une valeur de type IO ()
, représentant une action d’E/S qui “calcule” une valeur de type ()
(lire “unité” ; la tuple vide ne transmettant aucune information) en plus d’effectuer des effets secondaires sur le monde extérieur (ici, l’impression d’une chaîne sur le terminal). Cette annotation de type est généralement omise pour main
car c’est son seul type possible.
Mettez ceci dans un fichier helloworld.hs
et compilez-le à l’aide d’un compilateur Haskell, tel que GHC :
ghc helloworld.hs
L’exécution du fichier compilé entraînera l’affichage à l’écran de la sortie “Hello, World!”
./helloworld
Hello, World!
Alternativement, runhaskell
ou runghc
permettent d’exécuter le programme en mode interprété sans avoir à le compiler :
runhaskell helloworld.hs
Le REPL interactif peut également être utilisé à la place de la compilation. Il est livré avec la plupart des environnements Haskell, tels que “ghci” fourni avec le compilateur GHC :
ghci> putStrLn "Hello World!"
Hello, World!
ghci>
Vous pouvez également charger des scripts dans ghci à partir d’un fichier en utilisant load
(ou :l
):
ghci> :load helloworld
:reload
(ou :r
) recharge tout dans ghci :
Prelude> :l helloworld.hs
[1 of 1] Compiling Main ( helloworld.hs, interpreted )
<some time later after some edits>
*Main> :r
Ok, modules loaded: Main.
Explication:
Cette première ligne est une signature de type, déclarant le type de main
:
main :: IO ()
Les valeurs de type IO ()
décrivent des actions qui peuvent interagir avec le monde extérieur.
Parce que Haskell a un système de type Hindley-Milner à part entière qui permet l’inférence de type automatique, les signatures de type sont techniquement facultatives : si vous omettez simplement le main :: IO ()
, le compilateur pourra déduire lui-même le type en analysant la définition de main
. Cependant, il est très considéré comme un mauvais style de ne pas écrire de signatures de type pour les définitions de niveau supérieur. Les raisons incluent :
-
Les signatures de type dans Haskell sont une documentation très utile car le système de type est si expressif que vous pouvez souvent voir à quoi sert une fonction simplement en regardant son type. Cette « documentation » est facilement accessible avec des outils tels que GHCi. Et contrairement à la documentation normale, le vérificateur de type du compilateur s’assurera qu’il correspond bien à la définition de la fonction !
-
Tapez les signatures keep bugs local. Si vous faites une erreur dans une définition sans fournir sa signature de type, le compilateur peut ne pas signaler immédiatement une erreur mais simplement en déduire un type absurde, avec lequel il vérifie réellement le type. Vous pouvez alors obtenir un message d’erreur crypté lors de l’utilisation de cette valeur. Avec une signature, le compilateur est très bon pour repérer les bogues là où ils se produisent.
Cette deuxième ligne fait le travail réel :
main = putStrLn "Hello, World!"
Si vous venez d’un langage impératif, il peut être utile de noter que cette définition peut aussi s’écrire :
main = do {
putStrLn "Hello, World!" ;
return ()
}
Ou de manière équivalente (Haskell a une analyse basée sur la mise en page ; mais * méfiez-vous de mélanger les tabulations et les espaces de manière incohérente *, ce qui confondra ce mécanisme):
main = do
putStrLn "Hello, World!"
return ()
Chaque ligne d’un bloc do
représente un monadic (ici, I/O) calcul, de sorte que l’ensemble Le bloc do
représente l’action globale composée de ces sous-étapes en les combinant d’une manière spécifique à la monade donnée (pour les E/S, cela signifie simplement les exécuter l’une après l’autre).
La syntaxe do
est elle-même un sucre syntaxique pour les monades, comme IO
ici, et return
est une action sans opération produisant son argument sans effectuer d’effets secondaires ou de calculs supplémentaires qui pourraient faire partie d’une définition de monade particulière.
Ce qui précède revient à définir main = putStrLn "Hello, World!"
, car la valeur putStrLn "Hello, World!"
a déjà le type IO ()
. Considéré comme une “instruction”, putStrLn "Hello, World!"
peut être vu comme un programme complet, et vous définissez simplement main
pour faire référence à ce programme.
Vous pouvez rechercher la signature de putStrLn
en ligne :
putStrLn :: String -> IO ()
-- thus,
putStrLn (v :: String) :: IO ()
putStrLn
est une fonction qui prend une chaîne comme argument et génère une action I/O (c’est-à-dire une valeur représentant un programme que le runtime peut exécuter). Le runtime exécute toujours l’action nommée main
, nous devons donc simplement la définir comme égale à putStrLn "Hello, World!"
.
[1] : https://en.wikipedia.org/wiki/%22Hello,_World!%22_program
Factoriel
La fonction factorielle est un Haskell “Hello World!” (et pour la programmation fonctionnelle en général) dans le sens où il démontre succinctement les principes de base du langage.
Variante 1
fac :: (Integral a) => a -> a
fac n = product [1..n]
[Démo en direct] (http://coliru.stacked-crooked.com/a/7410a751e5007db6)
Integral
est la classe des types de nombres entiers. Les exemples incluentInt
etInteger
.(Integral a) =>
place une contrainte sur le typea
pour être dans ladite classefac :: a -> a
indique quefac
est une fonction qui prend una
et renvoie una
product
est une fonction qui accumule tous les nombres d’une liste en les multipliant ensemble.[1..n]
est une notation spéciale qui se transforme enenumFromTo 1 n
, et est la plage de nombres1 ≤ x ≤ n
.
Variante 2
fac :: (Integral a) => a -> a
fac 0 = 1
fac n = n * fac (n - 1)
[Démo en direct] (http://coliru.stacked-crooked.com/a/1e80dec346f844f6)
Cette variante utilise la correspondance de modèle pour diviser la définition de la fonction en cas distincts. La première définition est invoquée si l’argument est “0” (parfois appelée la condition d’arrêt) et la seconde définition sinon (l’ordre des définitions est significatif). Il illustre également la récursivité car “fac” se réfère à lui-même.
Il convient de noter qu’en raison des règles de réécriture, les deux versions de “fac” se compileront en un code machine identique lors de l’utilisation de GHC avec les optimisations activées. Donc, en termes d’efficacité, les deux seraient équivalents.
Commencer
REPL en ligne
Le moyen le plus simple de commencer à écrire Haskell est probablement d’aller sur le [site Web de Haskell][haskell] ou [Essayer Haskell][tryhaskell] et d’utiliser le REPL en ligne (read-eval-print-loop) sur la page d’accueil. Le REPL en ligne prend en charge la plupart des fonctionnalités de base et même certaines E/S. Il existe également un didacticiel de base disponible qui peut être lancé en tapant la commande “help”. Un outil idéal pour commencer à apprendre les bases de Haskell et essayer quelques trucs.
GHC(i)
Pour les programmeurs qui sont prêts à s’engager un peu plus, il y a GHCi, un environnement interactif fourni avec le [Glorious/Glasgow Haskell Compiler][ghc]. Le GHC peut être installé séparément, mais ce n’est qu’un compilateur. Afin de pouvoir installer de nouvelles bibliothèques, des outils comme [Cabal][cabal] et [Stack][stack] doivent également être installés. Si vous utilisez un système d’exploitation de type Unix, l’installation la plus simple consiste à installer [Stack][stack] en utilisant :
curl -sSL https://get.haskellstack.org/ | sh
Cela installe GHC isolé du reste de votre système, il est donc facile à supprimer. Toutes les commandes doivent cependant être précédées de stack
. Une autre approche simple consiste à installer une [plateforme Haskell][plateforme]. La plateforme existe en deux versions :
- La distribution minimale ne contient que GHC (pour compiler) et Cabal/Stack (pour installer et construire des packages)
- La distribution complète contient en outre des outils pour le développement de projets, le profilage et l’analyse de la couverture. Un ensemble supplémentaire de packages largement utilisés est également inclus.
Ces plates-formes peuvent être installées en [téléchargeant un programme d’installation][plate-forme] et en suivant les instructions ou en utilisant le gestionnaire de packages de votre distribution (notez que cette version n’est pas garantie d’être à jour) :
-Ubuntu, Debian, Mint :
sudo apt-get install haskell-platform
-
Fédora :
sudo dnf install haskell-platform
-
Chapeau rouge:
sudo yum install haskell-platform
-Arch Linux :
sudo pacman -S ghc cabal-install haskell-haddock-api \
haskell-haddock-library happy alex
-
Gentoo :
sudo layman -a haskell sudo emerge haskell-platform
-
OSX avec Homebrew :
brew cask install haskell-platform
-
OSX avec MacPort :
sudo port install haskell-platform
Une fois installé, il devrait être possible de démarrer GHCi en invoquant la commande ghci
n’importe où dans le terminal. Si l’installation s’est bien déroulée, la console devrait ressembler à quelque chose comme
me@notebook:~$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/ :? for help
Prelude>
éventuellement avec plus d’informations sur les bibliothèques chargées avant le Prelude>
. Maintenant, la console est devenue un REPL Haskell et vous pouvez exécuter du code Haskell comme avec le REPL en ligne. Pour quitter cet environnement interactif, on peut taper :q
ou :quit
. Pour plus d’informations sur les commandes disponibles dans GHCi, tapez :?
comme indiqué dans l’écran de démarrage.
Parce qu’écrire les mêmes choses encore et encore sur une seule ligne n’est pas toujours pratique, il peut être judicieux d’écrire le code Haskell dans des fichiers. Ces fichiers ont normalement l’extension .hs
et peuvent être chargés dans le REPL en utilisant :l
ou :load
.
Comme mentionné précédemment, GHCi fait partie du GHC, qui est en fait un compilateur. Ce compilateur peut être utilisé pour transformer un fichier .hs
avec du code Haskell en un programme en cours d’exécution. Parce qu’un fichier .hs
peut contenir beaucoup de fonctions, une fonction main
doit être définie dans le fichier. Ce sera le point de départ du programme. Le fichier test.hs
peut être compilé avec la commande
ghc test.hs
cela créera des fichiers objets et un exécutable s’il n’y a pas eu d’erreurs et que la fonction main
a été définie correctement.
Outils plus avancés
-
Il a déjà été mentionné précédemment en tant que gestionnaire de packages, mais [stack][stack] peut être un outil utile pour le développement Haskell de manières complètement différentes. Une fois installé, il est capable de
- installing (multiple versions of) GHC
- project creation and scaffolding
- dependency management
- building and testing projects
- benchmarking
-
IHaskell est un noyau haskell pour IPython et permet de combiner le code (exécutable) avec le démarquage et la notation mathématique.
[haskell] : http://www.haskell.org [tryhaskell] : http://tryhaskell.org/ [ghc] : http://www.haskell.org/ghc [plateforme] : http://www.haskell.org/platform [cabale] : http://www.haskell.org/cabale/ [pile] : http://haskellstack.org
Fibonacci, Utilisation de l’évaluation paresseuse
L’évaluation paresseuse signifie que Haskell n’évaluera que les éléments de liste dont les valeurs sont nécessaires.
La définition récursive de base est :
f (0) <- 0
f (1) <- 1
f (n) <- f (n-1) + f (n-2)
S’il est évalué directement, il sera très lent. Mais, imaginons que nous ayons une liste qui enregistre tous les résultats,
fibs !! n <- f (n)
Alors
┌──────┐ ┌──────┐ ┌──────┐
│ f(0) │ │ f(1) │ │ f(2) │
fibs -> 0 : 1 : │ + │ : │ + │ : │ + │ : .....
│ f(1) │ │ f(2) │ │ f(3) │
└──────┘ └──────┘ └──────┘
┌────────────────────────────────────────┐
│ f(0) : f(1) : f(2) : ..... │
└────────────────────────────────────────┘
-> 0 : 1 : +
┌────────────────────────────────────────┐
│ f(1) : f(2) : f(3) : ..... │
└────────────────────────────────────────┘
Ceci est codé comme:
fibn n = fibs !! n
where
fibs = 0 : 1 : map f [2..]
f n = fibs !! (n-1) + fibs !! (n-2)
Ou même comme
GHCi> let fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
zipWith
crée une liste en appliquant une fonction binaire donnée aux éléments correspondants des deux listes qui lui sont données, donc zipWith (+) [x1, x2, ...] [y1, y2, ...]
est égal à [x1 + y1, x2 + y2, ...]
.
Une autre façon d’écrire fibs
est avec la fonction scanl
:
GHCi> let fibs = 0 : scanl (+) 1 fibs
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
scanl
construit la liste des résultats partiels que foldl
produirait, en travaillant de gauche à droite le long de la liste d’entrée. Autrement dit, scanl f z0 [x1, x2, ...]
est égal à [z0, z1, z2, ...] où z1 = f z0 x1 ; z2 = f z1 x2 ; ...
.
Grâce à l’évaluation paresseuse, les deux fonctions définissent des listes infinies sans les calculer entièrement. Autrement dit, nous pouvons écrire une fonction fib
, récupérant le nième élément de la séquence de Fibonacci illimitée :
GHCi> let fib n = fibs !! n -- (!!) being the list subscript operator
-- or in point-free style:
GHCi> let fib = (fibs !!)
GHCi> fib 9
34
Nombres premiers
Quelques variantes les plus marquantes :
En dessous de 100
import Data.List ( (\\) )
ps100 = ((([2..100] \\ [4,6..100]) \\ [6,9..100]) \\ [10,15..100]) \\ [14,21..100]
-- = (((2:[3,5..100]) \\ [9,15..100]) \\ [25,35..100]) \\ [49,63..100]
-- = (2:[3,5..100]) \\ ([9,15..100] ++ [25,35..100] ++ [49,63..100])
Illimité
Tamis d’Eratosthenes, utilisant package data-ordlist:
import qualified Data.List.Ordered
ps = 2 : _Y ((3:) . minus [5,7..] . unionAll . map (\p -> [p*p, p*p+2*p..]))
_Y g = g (_Y g) -- = g (g (_Y g)) = g (g (g (g (...)))) = g . g . g . g . ...
Traditionnel
(un tamis de division d’essai sous-optimal)
ps = sieve [2..]
where
sieve (x:xs) = [x] ++ sieve [y | y <- xs, rem y x > 0]
-- = map head ( iterate (\(x:xs) -> filter ((> 0).(`rem` x)) xs) [2..] )
Division d’essai optimale
ps = 2 : [n | n <- [3..], all ((> 0).rem n) $ takeWhile ((<= n).(^2)) ps]
-- = 2 : [n | n <- [3..], foldr (\p r-> p*p > n || (rem n p > 0 && r)) True ps]
Transitoire
De la division d’essai au crible d’Eratosthène :
[n | n <- [2..], []==[i | i <- [2..n-1], j <- [0,i..n], j==n]]
Le code le plus court
nubBy (((>1).).gcd) [2..] -- i.e., nubBy (\a b -> gcd a b > 1) [2..]
nubBy
est également de Data.List
, comme (\\ )
.
Déclaration de valeurs
Nous pouvons déclarer une série d’expressions dans le REPL comme ceci :
Prelude> let x = 5
Prelude> let y = 2 * 5 + x
Prelude> let result = y * 10
Prelude> x
5
Prelude> y
15
Prelude> result
150
Pour déclarer les mêmes valeurs dans un fichier on écrit le Suivant:
-- demo.hs
module Demo where
-- We declare the name of our module so
-- it can be imported by name in a project.
x = 5
y = 2 * 5 + x
result = y * 10
Les noms de modules sont en majuscules, contrairement aux noms de variables.