read in English

Tutoriel pour Haskell

Acte I

1. Introduction

La programmation en Haskell est différente de la programmation impérative à laquelle vous pourriez être habitué. Haskell est un langage fonctionnel pur, ce qui signifie qu'il est basé sur des fonctions et évite les effets de bord.
Voici quelques bases de la programmation en Haskell :
1. Fonctions : En Haskell, les fonctions sont des citoyens de première classe. Cela signifie que vous pouvez les passer en tant qu'arguments à d'autres fonctions, les retourner comme valeurs à partir de fonctions, et les stocker dans des structures de données.
2. Immutabilité : Les données sont immuables par défaut en Haskell. Cela signifie qu'une fois qu'une valeur est définie, elle ne peut pas être modifiée. Au lieu de cela, les fonctions produisent de nouvelles valeurs à partir des données d'entrée.
3. Évaluation paresseuse : Haskell utilise une évaluation paresseuse (lazy evaluation). Cela signifie que les expressions ne sont évaluées que lorsque leur valeur est nécessaire. Cela permet d'écrire des programmes efficaces même avec des données potentiellement infinies.
4. Types statiques forts : Haskell est fortement typé, ce qui signifie que chaque expression a un type déterminé à la compilation. Cela garantit une grande sécurité et fiabilité du programme en détectant les erreurs de type à la compilation plutôt qu'à l'exécution.
5. Pattern matching : Haskell offre un puissant mécanisme de correspondance de motifs (pattern matching) qui permet de décomposer les structures de données de manière concise et expressive.
6. Listes infinies : Grâce à l'évaluation paresseuse, Haskell permet la manipulation de listes infinies. Vous pouvez définir une liste infinie et n'évaluer que les éléments nécessaires.
7. Récurtion : Comme dans la plupart des langages de programmation fonctionnels, la récursion est un concept clé en Haskell. Les boucles sont généralement remplacées par la récursion, ce qui permet d'écrire des fonctions de manière déclarative.
8. Monades : Les monades sont une abstraction puissante en Haskell pour gérer les effets de bord tels que l'entrée/sortie, la gestion des erreurs, ou le calcul probabiliste. Elles permettent de séquencer les calculs de manière sécurisée et concise.
En résumé, la programmation en Haskell repose sur des fonctions pures, des types statiques forts, l'évaluation paresseuse et d'autres concepts fonctionnels pour créer des programmes sûrs, efficaces et expressifs. Bien que cela puisse sembler différent au premier abord, Haskell offre une approche élégante et puissante de la programmation une fois que vous vous y habituez.

2. Exemple de Fonction

Voici un exemple simple de fonction en Haskell qui calcule la somme des éléments d'une liste :
-- Définition de la fonction sumList qui prend une liste d'entiers en entrée et retourne leur somme
sumList :: [Int] -> Int           -- Type de la fonction : liste d'entier en entrée, et retourne un entier
sumList [] = 0                    -- Cas de base : si la liste est vide, la somme est 0
sumList (x:xs) = x + sumList xs   -- Cas récursif : ajoute l'élément en tête de liste à la somme du reste de la liste

-- Exemple d'utilisation de la fonction
main = do
    let myList = [1, 2, 3, 4, 5]
    putStrLn $ "La somme de la liste " ++ show myList ++ " est " ++ show (sumList myList)

Dans cet exemple :
- La fonction `sumList` prend une liste d'entiers `[Int]` en entrée et retourne un entier `Int`.
- Elle utilise la correspondance de motif (pattern matching) pour définir deux cas :
- Si la liste est vide (`[]`), la somme est 0.
- Sinon, la fonction additionne le premier élément `x` de la liste à la somme des éléments restants (`xs`), en utilisant la récursion.
- L'exemple d'utilisation dans la fonction `main` définit une liste `myList` et affiche la somme de ses éléments à l'aide de la fonction `sumList`.
Compiler avec ghc :
$ ghc sum.hs
[1 of 1] Compiling Main           ( sum.hs, sum.o )
Linking sum ...

$ ./sum
La somme de la liste [1,2,3,4,5] est 15

3. Principales Structures de Données en Haskell

En Haskell, les principales structures de données comprennent les listes, les tuples et les types de données algébriques. Voici un aperçu de chacune de ces structures de données :
1. Listes : Les listes sont des collections ordonnées d'éléments du même type. Elles sont définies à l'aide de crochets et peuvent contenir zéro ou plusieurs éléments. Les listes peuvent être construites récursivement en utilisant le conséquent `:` pour ajouter un élément à une liste existante. Par exemple :
 [1, 2, 3, 4
2. Tuples : Les tuples sont des collections ordonnées d'éléments de types différents. Contrairement aux listes, les tuples ont une taille fixe et ne peuvent pas être modifiés. Ils sont définis à l'aide de parenthèses et peuvent contenir un nombre quelconque d'éléments. Par exemple :
 (1, "hello", True
3. Types de données algébriques : Haskell permet de définir des types de données personnalisés à l'aide de la déclaration `data`. Cela permet de créer des types de données composés contenant plusieurs champs, similaires aux structures en langages impératifs. Par exemple :
 data Person = Person String Int 
Ce code crée un nouveau type de données `Person` avec deux champs : un nom de type `String` et un âge de type `Int`.
En plus de ces structures de données de base, Haskell propose également des types de données plus avancés tels que les tableaux, les ensembles et les arbres, qui peuvent être définis à l'aide de bibliothèques tierces ou de types de données personnalisés.
Voici un exemple d'utilisation du type de données `Person` en Haskell :
-- Définition du type de données Person
data Person = Person String Int

-- Fonction pour afficher une personne
showPerson :: Person -> String
showPerson (Person name age) = "Nom: " ++ name ++ ", Age: " ++ show age

-- Exemple d'utilisation
main :: IO ()
main = do
    let alice = Person "Alice" 25
        ethan = Person "Ethan" 30
    putStrLn $ "Informations sur Alice : " ++ showPerson alice
    putStrLn $ "Informations sur Ethan : " ++ showPerson ethan

Dans cet exemple :
- Le type de données `Person` est défini avec deux champs : `name` de type `String` et `age` de type `Int`.
- Une fonction `showPerson` est définie pour afficher les informations d'une personne.
- Dans la fonction `main`, deux personnes, Alice et Ethan, sont créées à l'aide du constructeur `Person`.
- Les informations sur chaque personne sont affichées en utilisant la fonction `showPerson`.
L'exécution de ce programme affichera les informations sur Alice et Ethan :
Informations sur Alice : Nom: Alice, Age: 25
Informations sur Ethan : Nom: Ethan, Age: 30
Cet exemple montre comment définir et utiliser un type de données personnalisé en Haskell pour représenter des entités avec des attributs spécifiques.

4. Principaux types en Haskell

En Haskell, les types de données sont un concept fondamental. Voici quelques-uns des principaux types de données en Haskell :
1. Types de base :
- `Int` : Entiers signés de précision fixe.
- `Integer` : Entiers signés de précision illimitée.
- `Float` : Nombre à virgule flottante simple précision.
- `Double` : Nombre à virgule flottante double précision.
- `Char` : Caractères Unicode.
- `Bool` : Valeurs booléennes `True` ou `False`.
2. Types composés :
- Listes : Collection ordonnée d'éléments du même type.
- Tuples : Collection ordonnée d'éléments de types différents.
- Types de données algébriques : Types de données personnalisés définis à l'aide de la déclaration `data`.
3. Types de fonction :
- `->` : Type de fonction. Par exemple, `Int -> Int` représente une fonction prenant un entier en entrée et renvoyant un entier en sortie.
4. Types polymorphes :
- `a` : Type de variable polymorphe. Il peut représenter n'importe quel type.
- `Maybe a` : Type qui représente une valeur optionnelle pouvant être `Just a` (avec une valeur) ou `Nothing` (sans valeur).
5. Types complexes :
- Listes de listes : Par exemple, `[Int]` représente une liste d'entiers, `[[Int]]` représente une liste de listes d'entiers, etc.
- Fonctions d'ordre supérieur : Les fonctions peuvent prendre d'autres fonctions en tant qu'arguments ou les retourner comme résultats.
6. Types dérivés :
- Nouveaux types dérivés à partir des types de base ou composés existants à l'aide de mots-clés tels que `type` et `newtype`.
Ces types constituent la base de la programmation en Haskell et offrent une grande expressivité pour définir des structures de données et des fonctions avec un haut degré de sûreté et de précision typique du typage statique fort de Haskell.

5. Types Dérivés avec `type` et `newtype`

Voici des exemples d'utilisation des mots-clés `type` et `newtype` pour créer des types dérivés en Haskell :
1. Utilisation de `type` :
-- Définition d'un type dérivé pour représenter des noms d'utilisateur
type Username = String

-- Utilisation du type dérivé
showUsername :: Username -> String
showUsername username = "Nom d'utilisateur : " ++ username

main :: IO ()
main = do
    let user = "JohnDoe"
    putStrLn $ showUsername user
Dans cet exemple, `Username` est un alias de type pour `String`. Cela permet de donner un sens sémantique supplémentaire à une chaîne de caractères sans introduire de nouveaux types distincts.
2. Utilisation de `newtype` :
-- Définition d'un nouveau type dérivé pour représenter des âges d'utilisateurs
newtype Age = Age Int

-- Fonction pour afficher un âge
showAge :: Age -> String
showAge (Age age) = "Âge : " ++ show age

main :: IO ()
main = do
    let userAge = Age 30
    putStrLn $ showAge userAge
Dans cet exemple, `Age` est un nouveau type dérivé de `Int` à l'aide de `newtype`. Cela permet de créer un nouveau type distinct d'`Int` tout en conservant une représentation interne similaire. Cela peut être utile pour éviter les erreurs de type et améliorer la lisibilité du code.

6. Différence entre `type` et `newtype`

En Haskell, `type` est utilisé pour créer des alias de type, tandis que `newtype` est utilisé pour créer de nouveaux types distincts avec des comportements spécifiques.
1. Type Synonyme (type) :
- `type` est utilisé pour créer des alias de type. Ce qui signifie que les deux types sont interchangeables et se comportent de la même manière.
- Par exemple, `type UserName = String` crée un alias `UserName` pour le type `String`. Les deux types sont essentiellement identiques et peuvent être utilisés de manière interchangeable.
- Les synonymes de type sont principalement utilisés pour rendre le code plus lisible et pour exprimer l'intention du développeur.
2. Newtype :
- `newtype` est utilisé pour créer un nouveau type distinct, même s'il est basé sur un type existant.
- Il permet de définir des instances de type et des comportements spécifiques à ce nouveau type sans avoir à se soucier des instances du type sous-jacent.
- Par exemple, `newtype Meter = Meter Double` crée un nouveau type `Meter` qui est distinct de `Double`, même s'il a la même représentation sous-jacente.
- Les newtypes offrent une sécurité supplémentaire car ils empêchent les erreurs de type en garantissant que les types différents ne seront pas mélangés par accident.
En résumé, `type` crée un alias de type, tandis que `newtype` introduit un nouveau type distinct avec des propriétés spécifiques, offrant ainsi plus de sécurité et de clarté dans le code.

7. Type `Maybe`

`Maybe` est un type de données en Haskell utilisé pour représenter des valeurs qui peuvent être présentes ou absentes. C'est un moyen élégant de gérer les cas où une valeur peut être indéfinie ou nulle, tout en évitant les erreurs de référence nulle.
En termes simples, `Maybe` peut être soit :
1. `Just a`: représentant une valeur existante de type `a`.
2. `Nothing`: représentant l'absence de valeur.
Par exemple, supposons que vous ayez une fonction qui recherche un élément dans une liste et renvoie cet élément s'il est trouvé. Si l'élément n'est pas trouvé, la fonction pourrait renvoyer `Nothing` pour indiquer qu'aucune valeur correspondante n'a été trouvée.
Voici un exemple de fonction utilisant `Maybe` :
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide x y = Just (x / y)

Cette fonction `safeDivide` divise deux nombres, mais elle retourne `Nothing` si le deuxième nombre est zéro, ce qui évite une erreur de division par zéro.
Voici comment vous pouvez utiliser cette fonction :
main :: IO ()
main = do
    putStrLn "Enter two numbers:"
    num1 <- readLn
    num2 <- readLn
    case safeDivide num1 num2 of
        Just result -> putStrLn $ "Result of division: " ++ show result
        Nothing -> putStrLn "Cannot divide by zero!"

Cette utilisation de `Maybe` permet de manipuler des valeurs potentiellement manquantes de manière sûre et concise, en forçant le développeur à gérer explicitement les cas où une valeur peut être absente. Cela encourage la programmation robuste et la gestion appropriée des valeurs nulles.

8. Opérateur `$`

En Haskell, le symbole `$` est appelé "application de fonction de bas niveau" ou "opérateur de bas niveau". Il est utilisé pour éviter l'écriture de parenthèses superflues lors de l'application de fonctions.
Voici comment il fonctionne :
1. `f $ x` est équivalent à `f x`.
2. Il a une faible précédence, ce qui signifie que toute expression à sa droite est évaluée avant l'application de la fonction à sa gauche.
3. Il peut être utilisé pour éviter d'utiliser des parenthèses excessives, surtout lors de l'application de fonctions imbriquées.
Voici un exemple pour illustrer son utilisation :
-- Les deux expressions suivantes sont équivalentes :

-- Sans $
result1 = sum (map (+1) [1, 2, 3])

-- Avec $
result2 = sum $ map (+1) [1, 2, 3]

Dans cet exemple, `sum $ map (+1) [1, 2, 3]` est équivalent à `sum (map (+1) [1, 2, 3])`. Le `$` permet d'éviter les parenthèses supplémentaires autour de `map (+1) [1, 2, 3]`, ce qui rend le code plus lisible et évite les erreurs de parenthésage.

9. La fonction `foldl`, fonction de pliage gauche

La fonction `foldl` en Haskell est une fonction de la bibliothèque standard qui prend en arguments une fonction, un élément initial (aussi appelé accumulateur) et une liste, puis applique itérativement la fonction à chaque élément de la liste, en utilisant l'élément initial comme premier argument de la fonction, puis en le transmettant successivement à chaque itération.
Voici une présentation simple de la fonction `foldl` :
Voici sa signature :
foldl :: (b -> a -> b) -> b -> [a] -> b
Elle prend une fonction binaire `b -> a -> b` (où `b` est le type de l'accumulateur et `a` est le type des éléments de la liste), une valeur initiale de l'accumulateur de type `b`, une liste `[a]`, puis applique la fonction binaire de gauche à droite sur chaque élément de la liste, en accumulant, ou en transmettant le résultat dans l'accumulateur, qui peut aussi être un résultat intermédiaire.
Voici un exemple d'utilisation :
-- Somme des éléments d'une liste
sumList :: [Int] -> Int
sumList xs = foldl (+) 0 xs

-- Concaténation de tous les éléments d'une liste de chaînes de caractères
concatList :: [String] -> String
concatList xs = foldl (++) "" xs

Dans `sumList`, la fonction `foldl (+) 0 xs` accumule la somme des éléments de la liste `xs` en partant de zéro comme valeur initiale.
Dans `concatList`, la fonction `foldl (++) "" xs` concatène tous les éléments de la liste `xs` en partant d'une chaîne de caractères vide comme valeur initiale.

10. Correspondances de Motifs

La structure `case` en Haskell est utilisée pour effectuer des correspondances de motifs (pattern matching) sur une valeur. Cela permet de décomposer une valeur en ses composants et d'exécuter différents morceaux de code en fonction de la forme de cette valeur. La structure `case` est très puissante et flexible, permettant de manipuler des types de données complexes de manière claire et concise.
Exemple simple
Voici un exemple simple utilisant `case` pour correspondre à une valeur de type `Maybe` :
describeMaybe :: Maybe Int -> String
describeMaybe x = case x of
    Nothing -> "No value"
    Just n  -> "The value is " ++ show n
Explication
- `describeMaybe` est une fonction qui prend un `Maybe Int` comme argument.
- L'expression `case x of` décompose la valeur de `x`.
- Si `x` est `Nothing`, la fonction renvoie "No value".
- Si `x` est `Just n`, où `n` est un entier, la fonction renvoie "The value is " suivi de la représentation en chaîne de caractères de `n`.
Exemple avec tuples
Voici un exemple utilisant des tuples :
describeTuple :: (Int, Int) -> String
describeTuple pair = case pair of
    (0, 0) -> "Both numbers are zero"
    (0, _) -> "First number is zero"
    (_, 0) -> "Second number is zero"
    _      -> "Neither number is zero"
Explication
- `describeTuple` est une fonction qui prend un tuple `(Int, Int)` comme argument.
- L'expression `case pair of` décompose la valeur de `pair`.
- Les différents motifs `(0, 0)`, `(0, _)`, `(_, 0)`, et `_` correspondent à différentes formes du tuple.
- `(0, 0)` correspond au cas où les deux nombres sont zéro.
- `(0, _)` correspond au cas où le premier nombre est zéro et le second peut être n'importe quoi.
- `(_, 0)` correspond au cas où le second nombre est zéro et le premier peut être n'importe quoi.
- `_` est un motif joker qui correspond à n'importe quel tuple qui n'a pas été capturé par les motifs précédents.
Exemple avec types de données personnalisés
Supposons que nous ayons un type de données représentant une forme géométrique :
data Shape = Circle Double | Rectangle Double Double

area :: Shape -> Double
area shape = case shape of
    Circle r          -> pi * r * r
    Rectangle width height -> width * height
Explication
- `Shape` est un type de données personnalisé avec deux constructeurs : `Circle` et `Rectangle`.
- La fonction `area` prend un `Shape` et renvoie son aire.
- L'expression `case shape of` décompose la valeur de `shape`.
- Si `shape` est un `Circle` avec un rayon `r`, l'aire est calculée comme `pi * r * r`.
- Si `shape` est un `Rectangle` avec une largeur `width` et une hauteur `height`, l'aire est calculée comme `width * height`.
Avantages de `case`
- Clarté : La structure `case` rend le code clair et expressif, en particulier lorsqu'il y a de nombreux cas à considérer.
- Sécurité : Haskell vérifie que tous les cas possibles sont couverts, aidant à éviter les erreurs d'exécution.
- Flexibilité : `case` permet de manipuler des types de données complexes et des structures imbriquées de manière concise.
En résumé, la structure `case` en Haskell est un outil puissant pour la correspondance de motifs, permettant de gérer différentes formes de données de manière claire, concise et sécurisée.

11. Les Gardes

En Haskell, les guards (ou gardes en français) sont des expressions booléennes qui servent à ajouter des conditions aux définitions de fonctions ou aux constructions `case` pour faciliter les décisions conditionnelles. Elles sont souvent utilisées comme une alternative plus lisible aux expressions `if-then-else` lorsqu'il y a plusieurs conditions à vérifier.
Syntaxe de base des guards
Les guards sont introduits par un pipe (`|`) suivi d'une condition booléenne, et ensuite de l'expression qui doit être évaluée si la condition est vraie.
Voici un exemple simple illustrant l'utilisation des guards pour définir une fonction qui classifie les entiers :
classify :: Int -> String
classify n
    | n < 0    = "Negative"
    | n == 0   = "Zero"
    | n > 0    = "Positive"
Fonctionnement détaillé
- La fonction `classify` prend un entier `n` en argument.
- Elle utilise des guards pour vérifier les différentes conditions sur `n`.
  - Si `n` est inférieur à 0, elle renvoie "Negative".
  - Si `n` est égal à 0, elle renvoie "Zero".
  - Si `n` est supérieur à 0, elle renvoie "Positive".
Exemple plus complexe
Imaginons une fonction qui calcule le tarif en fonction de l'âge :
ticketPrice :: Int -> Double
ticketPrice age
    | age < 5    = 0.0   -- Gratuit pour les moins de 5 ans
    | age < 18   = 5.0   -- Tarif réduit pour les moins de 18 ans
    | age < 65   = 10.0  -- Tarif normal pour les 18-64 ans
    | otherwise  = 7.0   -- Tarif réduit pour les 65 ans et plus
Utilisation de `otherwise`
Le mot-clé `otherwise` est une convention en Haskell, il est défini comme `otherwise = True`. Il est utilisé pour la dernière garde comme une clause par défaut si aucune des précédentes conditions n'est vraie.
Guards et `case`
Les guards peuvent aussi être utilisés à la place d'une expression `case`. Par exemple :
describeList :: [a] -> String
describeList xs = case xs of
    []   -> "The list is empty."
    [x]  -> "The list has one element."
    xs   -> "The list has " ++ show (length xs) ++ " elements."

Peut être réécrit en utilisant des guards comme suit :
describeList :: [a] -> String
describeList xs
    | null xs        = "The list is empty."
    | length xs == 1 = "The list has one element."
    | otherwise      = "The list has " ++ show (length xs) ++ " elements."
Avantages des guards
- Lisibilité : Les guards peuvent rendre le code plus lisible et plus proche de la formulation naturelle des conditions.
- Clarté : Elles évitent l'imbrication de multiples `if-then-else`, ce qui peut rendre le code difficile à suivre.
- Flexibilité : Les guards peuvent être utilisées dans les définitions de fonctions ainsi que dans les expressions `case`, rendant leur utilisation très flexible.
En résumé, les guards sont un outil puissant en Haskell pour gérer les conditions de manière claire et concise, améliorant ainsi la lisibilité et la maintenabilité du code.

12. L'Interpréteur Interactif

GHCi, abréviation de "Glasgow Haskell Compiler Interactive", est un interpréteur interactif fourni avec le compilateur GHC (Glasgow Haskell Compiler).
GHCi peut être lancé en tapant `ghci` dans la ligne de commande :
$ ghci
GHCi, version 8.8.4, :? for help

Prelude> print "hello"
"hello"

Prelude> :type print
print :: Show a => a -> IO ()

Prelude> 2 + 2
4

Prelude> square x = x * x

Prelude> square 6
36

Prelude> :type square
square :: Num a => a -> a

Leaving GHCi.
Vous pouvez charger des fichiers source Haskell dans GHCi à l'aide de la commande `:load`. Par exemple, `:load myfile.hs` chargera le fichier `myfile.hs`.
Vous pouvez demander à GHCi d'afficher le type d'une expression à l'aide de la commande `:type`. Par exemple, `:type head` affichera le type de la fonction `head`.

13. Classe de type `Num a`

`Num a` est une classe de type en Haskell. En Haskell, les classes de type sont utilisées pour définir des comportements communs partagés par plusieurs types. La classe `Num` représente les types numériques, c'est-à-dire les types qui supportent les opérations arithmétiques comme l'addition, la soustraction, la multiplication, etc.
Ainsi, lorsque vous voyez `Num a`, cela signifie que `a` est un type qui est une instance de la classe `Num`, c'est-à-dire qu'il est numérique. Cela inclut des types comme `Int`, `Integer`, `Float`, `Double`, etc.
Par exemple, si une fonction a la signature `foo :: Num a => a -> a`, cela signifie que `foo` prend un argument de type numérique `a` et renvoie également un résultat de type numérique `a`, où `a` peut être n'importe quel type numérique.

14. Contraintes de classe de type

En Haskell, `=>` est utilisé pour spécifier des contraintes de classe de type. Lorsque vous voyez une signature de fonction ou un type avec `=>`, cela signifie que les types utilisés dans cette signature ou ce type doivent satisfaire les contraintes de classe spécifiées.
Par exemple, si vous voyez une signature de fonction comme ceci :
foo :: Num a => a -> a
Cela signifie que `foo` est une fonction prenant un argument de type `a`, où `a` doit être une instance de la classe de type `Num` (un type numérique), et renvoyant également un résultat de type `a` qui doit être du même type numérique.
En bref, `=>` est utilisé pour imposer des contraintes sur les types utilisés dans les signatures de fonction ou de type. Cela garantit que les opérations spécifiées dans les contraintes de classe sont disponibles pour les types utilisés.

15. Initialisser une liste de 12 nombres aléatoires

Pour initialiser une liste de 12 nombres aléatoires en Haskell, vous pouvez utiliser le module `System.Random` pour générer des nombres aléatoires et le module `Control.Monad` pour importer la fonction `replicateM`. Voici comment vous pourriez le faire :
import System.Random
import Control.Monad (replicateM)

-- Génère une liste de 12 nombres aléatoires entre 0 et 100 inclus
randomNumbers :: IO [Int]
randomNumbers = replicateM 12 $ randomRIO (0, 100)

Dans cet exemple :
- `randomRIO` est une fonction qui génère un nombre aléatoire dans la plage spécifiée.
- `replicateM` est une fonction qui répète une action un certain nombre de fois et collecte les résultats dans une liste.
- Le résultat est une liste de 12 nombres aléatoires entre 0 et 100 inclus.
Vous pouvez utiliser cette fonction dans votre programme pour obtenir une liste de nombres aléatoires en appelant simplement `randomNumbers`. Par exemple :
main :: IO ()
main = do
    numbers <- randomNumbers
    print numbers
Cela affichera une liste de 12 nombres aléatoires chaque fois que vous exécutez votre programme Haskell.
$ ghc r.hs -package random
[1 of 1] Compiling Main             ( r.hs, r.o )
Linking r ...

$ ./r
[37,99,100,64,28,93,67,53,60,25,33,19]

16. Quelques fonctions de traitement de base sur les listes

$ ghci
GHCi, version 8.8.4, :? for help

Prelude> li = [1,2,3,4,5,6]

Prelude> take 2 li
[1,2]

Prelude> drop 2 li
[3,4,5,6]

Prelude> reverse li
[6,5,4,3,2,1]

Prelude> map (+ 2) li
[3,4,5,6,7,8]

Prelude> foldl (\acc x -> acc + x) 0 li
21

Prelude> filter (\x -> (x `mod` 2) == 0) li
[2,4,6]

Prelude> 5 `elem` li  -- test la présence d'un élément dans une liste
True

Prelude> 12 `elem` li
False

Prelude> length li
6

Prelude> [0, 100] ++ li
[0,100,1,2,3,4,5,6]

Prelude> any (\x -> x `mod` 2 == 0) [1, 3, 5, 7]
False

Prelude> import Data.List (find)

Prelude Data.List> find (\x -> x == 4) li
Just 4

Prelude Data.List> :type find
find :: Foldable t => (a -> Bool) -> t a -> Maybe a

17. Nommer les champs d'une structure `data`

En Haskell, avec `data`, vous pouvez aussi nommer les champs dans chaque constructeur de données. Cela vous permet de créer des types de données avec des champs nommés pour améliorer la lisibilité et la maintenance de votre code.
Voici un exemple de déclaration de type de données avec des champs nommés :
data Person = Person { firstName :: String,
                       lastName :: String,
                       age :: Int,
                       email :: String
                     }

Dans cet exemple, `Person` est un type de données avec un seul constructeur de données nommé `Person`. Ce constructeur de données a quatre champs nommés : `firstName`, `lastName`, `age` et `email`, chacun ayant son propre type.
L'utilisation de champs nommés permet d'accéder facilement aux valeurs associées à chaque champ dans une instance de `Person`. Par exemple :
let john = Person { firstName = "John", lastName = "Doe", age = 30, email = "john@example.com" }
Vous pouvez ensuite accéder aux champs individuels comme ceci :
print (firstName john)  -- Affiche "John"
print (age john)  -- Affiche 30
L'utilisation de champs nommés dans les types de données `data` est un moyen pratique d'améliorer la clarté et la maintenabilité de votre code Haskell.

18. Structure Update

Voici comment créer une fonction qui retourne une nouvelle structure `data` avec un champ ayant une valeur différente de l'originale :
$ ghci
GHCi, version 8.8.4, :? for help

Prelude> data Person = Person { firstName :: String, lastName :: String, age :: Int } deriving (Show)

Prelude> let john1 = Person { firstName = "John", lastName = "Doe", age = 30 }

Prelude> newAge person _age = person { age = _age }

Prelude> let john2 = newAge john1 31

Prelude> john2
Person {firstName = "John", lastName = "Doe", age = 31}
`deriving (Show)` permet de pouvoir afficher le type.

19. Les Types Algébriques de Données

En Haskell, il est possible d'utiliser les types algébriques de données (ou Algebraic Data Types, ADTs, en anglais) pour définir des types de données qui peuvent prendre différentes formes.
Voici un exemple pour illustrer cette utilisation à l'aide du mot-clé `data` :
data Shape =
    Circle Float
  | Rectangle Float Float
  | Triangle Float Float Float

Cette définition permet de créer des valeurs du type `Shape` qui peuvent être un `Circle` avec un seul argument de type `Float`, un `Rectangle` avec deux arguments de type `Float`, ou un `Triangle` avec trois arguments de type `Float`.
Utilisation des valeurs
Pour créer et utiliser des valeurs de ce type en Haskell :
let myCircle = Circle 5.0

area :: Shape -> Float
area shape = case shape of
  Circle r -> 3.14 * r * r
  Rectangle w h -> w * h
  Triangle a b c ->   -- calcul de l'aire d'un triangle
    let s = (a + b + c) / 2
    in sqrt (s * (s - a) * (s - b) * (s - c))

myTriangle :: Shape
myTriangle = Triangle 3.0 4.0 5.0

let triangleArea = area myTriangle

(Dans la définition du type `Shape` pour un triangle, les 3 valeurs représentent les longueurs des trois côtés du triangle.)
Les types algébriques de données permettent de définir des types de données pouvant avoir plusieurs constructeurs avec différents arguments, ou zéro arguments :
data Dir = Left | Right

20. Extraire une sous-chaîne

$ ghci
GHCi, version 8.8.4, :? for help

Prelude> let subStr s i j = take j (drop i s)

Prelude> let s = "Hello World"

Prelude> subStr s 0 5
"Hello"

Prelude> subStr s 3 5
"lo Wo"

Prelude> :t subStr
subStr :: [a] -> Int -> Int -> [a]

Prelude> :t s
s :: [Char]

21. Récursivité Terminale

La récursivité terminale est importante en Haskell, car elle permet d'éviter le dépassement de pile (stack overflow) lors de l'exécution de fonctions récursives sur de grandes entrées. En Haskell, la récursivité terminale est souvent optimisée automatiquement par le compilateur grâce à l'optimisation de la queue de fonction (tail call optimization). Cependant, cela ne se produit que pour les fonctions qui sont écrites dans une forme particulière appelée "récursivité terminale". Pour bénéficier de cette optimisation, vous devez vous assurer que l'appel récursif est la dernière opération effectuée dans la fonction et qu'il n'y a pas d'opérations supplémentaires à effectuer après l'appel récursif.
-- not tail-rec
sumList1 li =
    case li of
        [] -> 0
        x:xs -> x + sumList1 xs

-- tail-rec
sumList2 li =
    let aux [] sum = sum
        aux (x:xs) sum = aux xs (x + sum)
    in aux li 0

main = do
    let li = [1,2,3,4,5,6]
    print $ sumList1 li
    print $ sumList2 li
Dans la fonction `sumList1`, il faut d'abord réaliser l'appel récursif `sumList1 xs` avant de pouvoir réaliser l'addition avec `x`. L'appel récursif n'est donc pas ici en position terminale.
En revanche, dans la fonction `sumList2`, il n'y a pas d'opération laissée en suspens. L'opération d'addition est réalisée en passant un paramètre supplémentaire, qui est retourné à la fin de tous les appels récursifs.

22. L'Indentation en Haskell,

En Haskell, l'indentation est cruciale pour définir la structure du code. Une indentation incorrecte peut conduire à des erreurs de syntaxe. Les blocs de code ne sont pas regroupés par des accolades ou des mots-clés comme dans d'autres langages. Les lignes de code qui appartiennent à un même bloc doivent être indentées au même niveau (comme en Python). Cela permet une lisibilité accrue, garantit la cohérence de la syntaxe, et aide à clarifier la structure du programme.

23. L'opérateur d'indexation

En Haskell, l'opérateur `!!` est utilisé pour accéder à un élément d'une liste par son index. Cet opérateur prend une liste et un indice comme arguments et retourne l'élément situé à cet indice. L'indexation commence à 0, ce qui signifie que l'indice 0 correspond au premier élément de la liste.
Voici un exemple d'utilisation de `!!` :
ghci> let list = [10, 20, 30, 40, 50]

ghci> list !! 0
10

ghci> list !! 2
30

ghci> list !! 4
50
Dans cet exemple, `list !! 0` renvoie `10`, qui est le premier élément de la liste. `list !! 2` renvoie `30`, qui est le troisième élément de la liste, et ainsi de suite.
Détails techniques
- Signature : L'opérateur `!!` a la signature `(!!) :: [a] -> Int -> a`. Cela signifie qu'il prend une liste de type `[a]` et un entier `Int`, et retourne un élément de type `a`.
- Sécurité : Si l'indice est en dehors des limites de la liste, cela déclenche une erreur au moment de l'exécution. Par exemple :
ghci> list !! 5
*** Exception: Prelude.!!: index too large
- Performance : L'accès à un élément via `!!` a une complexité temporelle de (O(n))(n) est la position de l'élément, car il doit parcourir la liste jusqu'à l'indice spécifié. Cela peut être inefficace pour de longues listes.
Exemple de code
Voici un exemple de programme Haskell utilisant `!!` :
main :: IO ()
main = do
  let numbers = [1, 2, 3, 4, 5]
  putStrLn $ "The first element is: " ++ show (numbers !! 0)
  putStrLn $ "The third element is: " ++ show (numbers !! 2)
  putStrLn $ "The fifth element is: " ++ show (numbers !! 4)

Ce programme affichera :
The first element is: 1
The third element is: 3
The fifth element is: 5
Alternatives
Pour des structures de données plus performantes où l'accès rapide à un élément par index est nécessaire, on peut envisager d'autres structures de données comme les vecteurs (`Vector`), qui offrent un accès en (O(1)) :
import qualified Data.Vector as V

main :: IO ()
main = do
  let numbers = V.fromList [1, 2, 3, 4, 5]
  putStrLn $ "The first element is: " ++ show (numbers V.! 0)
  putStrLn $ "The third element is: " ++ show (numbers V.! 2)
  putStrLn $ "The fifth element is: " ++ show (numbers V.! 4)

En utilisant `Data.Vector`, l'accès aux éléments par index est beaucoup plus efficace.

24. Le Type `IO ()`

En Haskell, le type `IO ()` est utilisé pour représenter des actions d'entrée/sortie qui ne produisent pas de valeur significative.
1. `IO` : C'est un type qui encapsule des actions d'entrée/sortie. Une valeur de type `IO a` représente une action qui, lorsqu'elle est exécutée, peut interagir avec le monde extérieur (comme lire un fichier, imprimer à l'écran, etc.) et produit une valeur de type `a`.
2. `()`, le type unité : Il représente une valeur unique, aussi appelée unité, souvent utilisée pour indiquer qu'il n'y a pas de valeur significative produite. C'est un peu comme `void` dans d'autres langages de programmation.
Quand on combine les deux pour obtenir `IO ()`, cela signifie que nous avons une action d'entrée/sortie qui, lorsqu'elle est exécutée, ne produit pas de valeur significative (ou, plus précisément, produit la valeur `()`, qui est triviale et sans intérêt en soi).
Exemples
Voici un exemple simple :
main :: IO ()
main = putStrLn "Hello, World!"

Dans ce code :
- `putStrLn "Hello, World!"` est une action d'entrée/sortie qui imprime une chaîne de caractères à l'écran.
- `main` a le type `IO ()`, ce qui signifie qu'il est une action d'entrée/sortie qui, une fois exécutée, ne produit pas de valeur significative (la valeur de retour est `()`).
Pourquoi `IO ()` ?
Le type `IO ()` est utile pour des actions où l'effet de l'action est important (comme écrire dans un fichier ou afficher un message), mais où la valeur produite par cette action n'est pas utilisée. En d'autres termes, `IO ()` signifie que nous sommes intéressés par les effets secondaires de l'action, pas par une valeur de retour.
Gestion des effets de bord
Les actions `IO` sont essentielles en Haskell pour gérer les effets de bord tout en gardant le langage pur. Les fonctions pures n'ont pas d'effets de bord, ce qui permet des raisonnements mathématiques sur les programmes. En isolant les effets de bord dans des actions `IO`, Haskell permet de maintenir cette pureté.
Exemple avec des effets combinés
Voici un autre exemple qui montre une combinaison de plusieurs actions `IO ()` :
main :: IO ()
main = do
    putStrLn "What is your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name ++ "!")

Dans ce code :
- `putStrLn` et `getLine` sont des actions d'entrée/sortie.
- La séquence de ces actions est combinée à l'aide de `do`-notation.
- Le type de `main` est toujours `IO ()` car la valeur finale produite est `()`.
En résumé, `IO ()` en Haskell est un type utilisé pour représenter des actions d'entrée/sortie qui n'ont pas de valeur de retour significative, mais qui effectuent des opérations importantes en interagissant avec le monde extérieur.

25. Les Exceptions

En Haskell, les exceptions sont utilisées pour gérer des situations exceptionnelles ou des erreurs dans un programme. Voici comment elles fonctionnent :
1. Levée d'exception : Pour signaler une erreur ou une situation exceptionnelle, vous pouvez lever une exception à l'aide de la fonction `throw` dans le module `Control.Exception`. Par exemple :
-- Fonction de division
divide :: Int -> Int -> Int
divide _ 0 = throw DivideByZero
divide x y = x `div` y

Ici, si le deuxième argument de la fonction `divide` est 0, une exception `DivideByZero` est levée.
2. Capture d'exception : Pour capturer et gérer une exception, vous pouvez utiliser la fonction `catch` également fournie par `Control.Exception`. Elle prend deux arguments : une action à exécuter et une fonction de gestion des exceptions.
import Control.Exception (ArithException(..), tryJust, evaluate, throw)
import Control.Monad (guard)

-- Mettre la fonction divide ici.

main :: IO ()
main = do
    -- Attempting to divide by zero.
    result <- tryJust (\e -> guard (e == DivideByZero) >> return e) (evaluate (divide 10 0))
    case result of
        Left DivideByZero -> putStrLn "Division par zéro !"
        Right value -> putStrLn $ "Result: " ++ show value

Ici, `tryJust` permet de capturer spécifiquement l'exception `DivideByZero` et la fonction `evaluate` permet d'évaluer l'expression en évaluant une exception si nécessaire.
3. Types d'exceptions : En Haskell, les exceptions sont typées. Cela signifie que chaque type d'exception est associé à un type spécifique. Par exemple, `DivideByZero` est un type d'exception qui représente une division par zéro.
4. Gestion sécurisée des exceptions : Les exceptions en Haskell sont gérées de manière pure et sûre. Cela signifie que les exceptions ne peuvent pas être utilisées pour des opérations non sécurisées comme dans d'autres langages de programmation. Les exceptions en Haskell sont utilisées principalement pour la gestion des erreurs dans le cadre d'une programmation fonctionnelle sûre.
Un autre exemple avec `catch` :
import Control.Exception (ArithException(..), catch, evaluate)

-- Fonction de division
divide :: Int -> Int -> Int
divide x y = x `div` y

main :: IO ()
main = do
    putStrLn "Attempting to divide by zero."
    -- Evaluation de la division et gestion de l'exception
    (evaluate (divide 10 0) >>= print) `catch` handler

-- Gestionnaire d'exceptions
handler :: ArithException -> IO ()
handler DivideByZero = putStrLn "Caught exception: Division by zero"
handler exn = putStrLn $ "Caught another arithmetic exception: " ++ show exn

En résumé, les exceptions en Haskell fournissent un mécanisme sûr et efficace pour gérer les erreurs et les situations exceptionnelles dans les programmes Haskell, en les distinguant clairement du flot normal de contrôle.

26. Installer des bibliothèques Haskell

Pour installer une bibliothèque Haskell, vous pouvez utiliser le gestionnaire de paquets de votre distribution, ou bien utiliser le gestionnaire de packages Haskell nommé `cabal`. Voici les étapes à suivre :
$ sudo apt-get update

$ sudo apt-cache search ghc
ghc - The Glasgow Haskell Compilation system

$ sudo apt-get install ghc

$ sudo apt-cache search cabal
cabal-install - command-line interface for Cabal and Hackage

$ sudo apt-get install cabal-install

$ cabal update
Downloading the latest package list from hackage.haskell.org

$ cabal install --lib gloss
Hackage est le répertoire de bibliothèques d'Haskell.

Acte II — Scène 1

27. Hello World avec SDL2

Voici un exemple minimaliste d'utilisation de la bibliothèque graphique SDL2, qui ouvre une fenêtre et affiche un rectangle :
{-# LANGUAGE OverloadedStrings #-}
import SDL.Vect (V2(..), V4(..), Point(P))
import SDL (($=))
import qualified SDL
import Control.Monad (unless)

main :: IO ()
main = do
    SDL.initialize [SDL.InitVideo]
    window <- SDL.createWindow "Hello World" SDL.defaultWindow { SDL.windowInitialSize = V2 320 240 }
    SDL.showWindow window
    renderer <- SDL.createRenderer window (-1) SDL.defaultRenderer
    loop renderer
    SDL.destroyRenderer renderer
    SDL.destroyWindow window
    SDL.quit

loop :: SDL.Renderer -> IO ()
loop renderer = do
    SDL.rendererDrawColor renderer $= V4 0 0 255 255
    SDL.clear renderer
    SDL.rendererDrawColor renderer $= V4 0 0 0 255
    let squareRect = SDL.Rectangle (P (V2 80 60)) (V2 60 40)  -- pos:(80, 60), size:(60x40)
    --SDL.drawRect renderer (Just squareRect)
    SDL.fillRect renderer (Just squareRect)
    SDL.present renderer
    event_loop renderer

event_loop :: SDL.Renderer -> IO ()
event_loop renderer = do
    maybeEvent <- SDL.pollEvent
    case maybeEvent of
        Just event -> unless (SDL.eventPayload event == SDL.QuitEvent) (event_loop renderer)
        Nothing -> loop renderer
Installer SDL2 pour Haskell :
$ sudo apt-cache search sdl2
libghc-sdl2-dev - high- and low-level bindings to the SDL 2 library

$ sudo apt-get install libghc-sdl2-dev
Compiler l'exemple :
ghc hello.hs -package sdl2

Ces exemples couvrent les bases d'Haskell, vous pouvez maintenant expérimenter et essayer d'aller plus loin !
Vous pouvez par exemple trouver divers exemples (avec des équivalences dans d'autres langages) sur le site RosettaCode.org.
Une bonne référence à laquelle se référer est le wikibook d'Haskell.
Apprendre Haskell vous fera le plus grand bien !
Exemple de jeux avec Haskell: wiki.haskell.org::Games.

Created with ChatGPT