par , 30/03/2015 à 12h02 (1159 Affichages)
- vous prendrez bien une petite cuillère de Haskell ?
Dans le précédent billet, j’ai utilisé LISP pour montrer ce que pouvait être une approche bottom-up fondée sur la modularité qu’apporte la composition de fonction. Pourtant, LISP n’est pas –et de loin- le langage qui favorise le plus cette approche. Dans ce billet et le suivant, je voudrais introduire certaines tournures syntaxiques des langages plus fonctionnels –j’utiliserai Haskell, mais vous pourriez trouver des choses équivalentes dans Ocaml, Scala, F# ou Erlang. Je reprendrai l’exemple de l’évaluation d’une main au poker, développé dans le billet précédent, pour faciliter la comparaison.
A tout seigneur, tout honneur
NB : une implémentation partielle d’un shell Haskell est disponible à l’adresse https://tryhaskell.org/
Une fonction s’appelle sans parenthèses, ni extérieures, ni postérieures :
>sqrt 4
2
Les opérateurs sont également des fonctions dont la seule particularité est que leur nom est composé uniquement de caractères spéciaux :
> 1 + 1
2
Ils peuvent être appelés comme des fonctions à condition de les placer entre parenthèses :
> (+) 1 1
2
Et une fonction peut être appelée comme un opérateur à condition de placer son nom entre des « backquotes », des apostrophes inclinées comme des accents graves :
> 5 `min` 6
5
Des fonctions monogames
Les fonctions n’ont en Haskell qu’un seul argument, même quand elles semblent en avoir plusieurs… Par exemple, si vous écrivez
> min 5 6
« min 5 » est d’abord évaluée, retournant une fonction qui prend un argument et qui équivaut à :
> \x -> min 5 x -- vous découvrez la syntaxe des fonctions lambda : \ arg1 arg2 … argN -> expression
Cette particularité est à la base de l’application « partielle » des fonctions :
> map (+1) [1,2,3]
[2,3,4]
Ou bien encore on peut définir une fonction inc (incrémente) de la façon suivante :
inc = (+1) -- nommer les arguments n’est pas nécessaire
Pour autant, par facilité de langage, je ferai par la suite comme si les fonctions pouvaient prendre plusieurs arguments.
Des fonctions typées
Il est encore trop tôt pour introduire le système de types de Haskell, qui a sa part de complexité, je le signale simplement et donne un aperçu de sa syntaxe :
processHand :: (Int -> Int -> Bool) -> [Int] -> [[Int]]
processHand (qui tient le même rôle que dans le billet précédent, préparer une main à son évaluation), prend comme argument :
- une fonction qui prend en argument deux Int et retourne un Bool : (Int -> Int -> Bool)
- une liste de Int : [Int]
- et retourne une liste de listes de Int : [[Int]]
Donner le type d’une fonction lors de sa déclaration n’est le plus souvent pas nécessaire (le compilateur fait un travail d’inférence de type pour s’en charger à notre place) mais c’est une bonne pratique et l’intervention de l’Homme est de temps en temps encore nécessaire (n’ayez pas trop peur de la grande méchante A.I.)
Des fonctions joliment composées
Haskell propose un opérateur de composition, le point :
(f . g) x < = > f (g x) -- la parenthèse autour de (f . g) est nécessaire car l’application de la fonction a une précédence plus élevée que la composition
On peut utiliser autant de fonctions que souhaité dans la composition. Pour reprendre l’exemple de processHand :
processHand p = reverse . sortBy (comparing length) . splitIf p . sort -- joli, non ?
NB : reverse inverse l’ordre d’une liste, sortBy trie une liste selon un critère donné en premier argument, splitIf est une fonction « maison » qui est le complément de split-if-not définie dans le billet précédent et sort trie la liste dans l’ordre ascendant.
La lecture doit donc se faire de droite à gauche, mais est aisée : sort, puis splitIf p, … puis reverse.
Si vous nommez l’argument de la fonction composée, il faut l’introduire par l’opérateur $. C’est un opérateur d’application de fonction, mais de précédence très faible :
1 2
| f . g x < = > f . (g x) -- oups !
f . g $ x < = > (f . g) x |
Comment traduire « pattern matching » ?
L’expression « pattern matching » pourrait peut-être être traduite comme « détection de motif » ; mais j’ai bien cherché sans trouver de traduction satisfaisante. Alors à votre bon cœur, messieurs-dames, laissez vos trouvailles en commentaire.
1 2
| fac 0 = 1 --lorsque largument est 0
fac n = n * fac (n-1) --pour tout autre argument |
C’est bien une seule fonction qui est définie. L’opération à réaliser est déterminée en fonction de l’argument effectivement reçu par la fonction. Le « pattern matching » peut être aussi compliqué que nécessaire et analyser, par exemple, une liste qui lui est donnée :
1 2
| len [] = 0
len ( x :xs) = 1 + len xs -- lie x au premier élément de la liste, xs au reste de la liste |
Récapitulons
Pour récapituler ce que nous avons appris dans ce billet, et introduire le mot-clé « where » qui permet de définir des termes utilisés précédemment dans la fonction, je vous propose une première implémentation de split-if :
1 2 3 4 5 6 7 8
|
splitIf :: (Int -> Int -> Bool) -> [Int] -> [[Int]]
splitIf _ [x] = [[x]] -- _ ou « wildcard » montre quon ignore largument
splitIf p (x:xs) = if not $ p x y then addConcat x next else addSplit x next
<div style="margin-left:40px">where next = splitIf p xs;
<div style="margin-left:40px">y = head . head $ next;
addConcat x (y:ys) = (x:y):ys ;
addSplit x xs = [x] : xs</div></div> |
Promis, nous ferons mieux la prochaine fois !
Exercice :
- installer Haskell (je vous conseille fortement ghc –compilateur- et ghci –interpréteur en ligne de commande), ainsi qu’un éditeur qui prenne en charge la syntaxe Haskell (Notepad++, Editra)
- feuilletez les bibliothèques standard d’Haskell grâce à Hoogle qui permet également de voir le code source
- reposez-vous bien avant le prochain billet, qui contiendra deux trois choses pour se tordre convenablement le cerveau