Jouons avec le pattern matching en Haskell

Posted by Nicolas Kosinski on 2016-09-27 Translations: en

Découvrons le pattern matching en Haskell en reprenant l'exemple des cartes à jouer utilisé dans l'article "Jouons avec le pattern matching en Scala".


Préambule : création d'une énumération

Codons notre énumération correspondant à nos quatre enseignes (carreau, cœur, pique et trèfle) :

data Enseigne = Carreau | Coeur | Pique | Trèfle
  deriving (Eq, Enum, Show)

Nous venons de créer notre propre type (data type) qui :

  • a quatre constructeurs (value constructors)
  • hérite des classes de base :
    • Eq pour implémenter l'égalité entre deux valeurs
    • Enum pour que les valeurs sont finies et ordonnées (sequentially ordered types)
    • Show pour avoir une représentation sous forme de chaîne de caractères, ce qui peut être utile pour débugguer ou pour évaluer interactivement du code via le REPL.

Utilisons maintenant ghci (Glascow Haskell Compiler Interactive environment), le REPL d'Haskell, pour interagir avec cette énumération :

*Main> Coeur == Coeur
True
*Main> succ Coeur
Pique


Exemples de pattern matching

Premier exemple basique

La fonction suivante retourne le symbole d'une enseigne :

symbole :: Enseigne -> String
symbole enseigne =
  case enseigne of
    Carreau ->"♦"
    Coeur -> "♥"
    Pique -> "♠"
    Trèfle -> "♣"    

Exemple d'appel :

*Main> putStrLn $ symbole $ Coeur

Notons que :

  • L'opérateur $ nous permet de chaîner nos fonctions, plutôt que de les imbriquer dans des parenthèses (putStrLn(symbole(Coeur))).
  • la fonction putStrLn permet d'afficher des caractères Unicode, à l'inverse de la fonction standard show qui ne retourne que des chaînes ASCII. 😎


Remarque : le compilateur sait détecter un pattern matching non exhaustif. Par exemple, le code suivant :

symbole :: Enseigne -> String
symbole enseigne = case enseigne of
    Carreau ->"♦"

génère un avertissement de compilation :

warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In a case alternative:
        Patterns not matched:
            Coeur
            Pique
            Trèfle

Et l'appel de cette fonction génére une exception :

*Main> symbole Coeur
"*** Exception: test.hs:(5,20)-(6,17): Non-exhaustive patterns in case


Deuxième exemple, partage d'expression via un bloc 'where'

Autre exemple, implémentons une fonction couleur qui retourne la couleur d'une enseigne (chaîne de caractères "rouge" ou "noir") :

couleur :: Enseigne -> String
couleur enseigne = case enseigne of
    Carreau -> rouge
    Coeur -> rouge
    Pique -> noir
    Trèfle -> noir
    where
      rouge = "rouge"
      noir = "noir"

Exemple d'appel :

*Main> couleur(Coeur)
"rouge"

Nous avons ici utilisé le mot-clé where qui nous permet de partager des expressions.

Troisième exemple, déstructuration

Définissons notre propre type Carte combinant un rang (1, 2, 3, ..., valet, dame, roi) et une enseigne :

data Rang =
  R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10 | Valet | Dame | Roi
  deriving (Eq, Ord, Enum, Show)

data Carte = Carte {
  rang :: Rang,
  enseigne :: Enseigne
} deriving (Eq, Show)

Le type Carte utilise la syntaxe record permettant de nommer les champs.

Nous pouvons ainsi utiliser le pattern matching pour "déstructurer" une carte en filtrant les champs. Par exemple, la fonction suivante permet de déterminer si deux cartes, associées par un tuple, sont de même enseigne :

mêmeEnseigne :: (Carte, Carte) -> Bool
mêmeEnseigne ((Carte _ enseigne1), (Carte _ enseigne2)) =
  enseigne1 == enseigne2

Exemples d'appel :

*Main> :{
*Main| mêmeEnseigne (
*Main|     Carte {rang=R1, enseigne=Carreau},
*Main|     Carte {rang=R1, enseigne=Coeur} )
*Main| :}
False
*Main> :{
*Main| mêmeEnseigne (
*Main|     Carte {rang=Valet, enseigne=Coeur},
*Main|     Carte {rang=R1, enseigne=Coeur} )
*Main| :}
True

Les rangs, que l'on n'utilise pas dans la fonction, ont été filtrés via le caractère wild-card (_).

Et voilà ! 🤓