Régulièrement à Octo, on organise des dojos, des séances d'entrainement pour coder. On a pu réaliser un jeu de la vie en javascript, faire un peu de BDD avec rSpec. Un jour on s'est dit mais pourquoi ne pas essayer un langage purement fonctionnel ? Comme on a plusieurs fan d'Haskell dans nos murs, nous avons choisi celui-ci.
C'est comme ça que nous avons organisé notre premier dojo en Haskell. Et aussi étrange que cela puisse paraître, nous avons réussi à dompter ce langage à la syntaxe et au paradigmes étranges, pour nous développeurs objet.
Regardons plus en détail ce que nous avons fait et appris.
Le but du dojo était de lire un fichier contenant des ordres de virements entre 2 comptes. Il fallait parser chaque ligne ne la traiter que si elle était valide. Chaque ligne devait comporter 6 champs, dans l'ordre :
Ex :
01 BANQUE1 COMPTE1 BANQUE2 COMPTE2 120 01 BANQUE2 COMPTE2 BANQUE3 COMPTE3 120 01 BANQUE3 COMPTE3 BANQUE1 COMPTE1 120
Comme c'était une séance de découverte de la syntaxe nous avons fait le choix de ne pas faire de tests unitaires, mais de faire des tests manuels du code. Puis une fois le code produit, nous nous sommes attaqués à écrire les tests unitaires.
Notre première étape a été de décrire la structure Account représentant un compte en banque (nom de la banque, nom du compte). En Haskell, il n'y a pas d'objets. Il n'y a que des structures de données (comme on peut en avoir en C) :
data Account = Account { accountBank :: String, accountName :: String }
Ensuite on a voulu que notre type Account puisse s'afficher comme on le souhaite dans la console : BANQUE1:COMPTE1. On lui a donc donné le comportement de Show, c'est à dire d'être affichable. On pourrait dire que l'on a étendu notre type Account avec l'interface Show. La majeure différence avec une interface au sens Java est que celle-ci comporte une implémentation par défaut :
instance Show Account where show account = (accountBank account) ++ ":" ++ (accountName account)
Ensuite, on créé un type Cre qui contient deux Account et la valeur du virement :
data Cre = Cre { creAccountFrom :: Account, creAccountTo :: Account, creAmount :: Int } deriving (Eq, Show)
Maintenant que l'on sait afficher correctement un Account, on a implémenté les contraintes sur le nom de la banque et du compte. Notre type Account n'étant pas une classe, on ne peut pas lui associer de méthodes, il faut donc créer des fonctions hors de tout contexte :
isValidBankName :: String -> Bool
isValidBankName = startswith "BANQUE"
isValidAccountName :: String -> Bool
isValidAccountName = startswith "COMPTE"
C'est ici que commencent les subtilités d'Haskell. Premièrement, on n'utilise pas de parenthèses autour de la liste des arguments d'une méthode, ni de virgule pour les séparer. Seul le caractère espace est utilisé. En Haskell, les parenthèses sont uniquement utilisées pour grouper un appel de fonction avec ses paramètres (ex: maMethode (autreMethode 3) 4). Notre fonction ne prend aucun paramètre pourtant la signature de celle-ci déclare qu'elle prend un type String en entrée. On aurait effectivement pu écrire la fonction comme ceci, en spécifiant le paramètre de la fonction.
getInt :: String -> Maybe Int
getInt string = if all isDigit string then Just (read string) else Nothing
Quelque part on fait de la duplication de code avec ce paramètre bank. Les langages fonctionnels apportent une réponse avec la curryfication, qui permet de se passer de se paramètre. On transforme la fonction startswith à deux paramètres en une nouvelle fonction (isValidBankName) à un seul paramètre en l'appliquant partiellement avec "BANQUE".
On a aussi besoin de vérifier que le montant du virement est bien un nombre et de le transformer de String en Int. On défini donc une méthode getInt qui d'une String nous renvoie un entier (ou pas) :
isValidBankName bank = startswith "BANQUE" bank
Enfin, on fait la glue entre tout ça dans la méthode getCre qui à partir d'une String nous renvoie (ou non) une opération de virement (un Cre) :
getCre :: String -> Maybe Cre
getCre string = if isValid then Just cre else Nothing
where
isValid = length ws == 6 && isValidAmount && isValidAccounts && isValidPrefix
ws = splitOn " " string
[p, b1, c1, b2, c2, a] = ws
isValidPrefix = p == "01"
isValidAmount = isJust $ getInt a
isValidAccounts = all isValidBankName [b1, b2] && all isValidAccountName [c1, c2]
cre = Cre (Account b1 c1) (Account b2 c2) (fromJust $ getInt a)
Avant de comprendre cette fonction, il faut savoir qu'Haskell est un langage à évaluation paresseuse. Il n'exécute pas de code avant que les résultats de ce code ne soient réellement nécessaires. Par exemple, la variable isValidAmount ne sera évaluée que si length ws == 6 est True.
Finalement, on parcourt toute notre chaine d'entrée :
parsedFile = map getCre ls
where ls = splitOn "\n" inputSample
Après avoir passé à faire des bouts de code Haskell, on a tous été impressionné par les possibilités de ce langage. Et surtout, j'ai été fasciné par la lisibilité du code final. Qui même si on ne lit pas la syntaxe reste néanmoins compréhensible.
Le code source du dojo est disponible sur github. Le repo contient aussi le code de quelques tests que nous avons codé.
Finalement, Haskell c'est pas bien compliqué. Il suffit d'être bien guidé, merci à Sebastian de l'avoir fait.