• Aucun résultat trouvé

Haskell et les bases des langages fonctionnels

4.2.1 Syntaxe du Haskell

Nous avons choisi la syntaxe du langage Haskell pour exprimer les algorithmes dans cette thèse. Le nom de ce langage, Haskell, est en honneur de Haskell Brooks CurryWik06g dont les travaux dans la

logique mathématique servent comme bases aux langages fonctionnels. Il s’agit d’un langage fonctionnel qui émane du Lambda calcul, qui est bien défini dans sa version actuelle nommée Haskell98Jon03mais

qui est également bien vivant car les travaux de définition de son successeur se poursuivent ; il est utilisé avec succès dans la recherche y compris par les membres2de Microsoft ResearchHMJH05, par exemple.

Notre choix du Haskell a été le résultat d’une petite recherche que nous avons menée et dont le but était de choisir un formalisme satisfaisant un certain nombre de desiderata. Dans notre cas, c’est Haskell qui a contenté nos exigences par la combinaison des propriétés suivantes :

• les description implicitement formelle des algorithmes à travers Haskell,

• la proximité de la notation utilisée en mathématiques où même les lecteurs non instruits peuvent

facilement comprendre les algorithmes exprimés en Haskell,

• les capacités larges de modélisation qui sont adaptées à nos besoins,

• pouvoir vérifier la bonne construction d’une définition en utilisant le parser pour l’analyse de

syntaxe,

• la possibilité de simulation par l’exécution d’un programme, ce qui nous permet de vérifier le bon

fonctionnement sur les données réelles.

La combinaison de ces propriétés nous a mené par la suite à l’utilisation du Haskell pour notre travail et pour nos explications.

Pourtant, nous ne sommes pas les seuls de se poser de telles exigeances et de choisir Haskell comme un outil ; il y a d’autres chercheurs qui l’utilisent pour la description mathématique, cf. l’article électro- nique intitulé Eleven Reasons to use Haskell as a MathematicianUnk06.

Le Lambda calculBB94travaille avec deux opérations de base, l’application et l’abstraction. L’opé-

ration d’application qui est exprimée dans le Lambda calcul par A · E

ou également par plus court A E

indique l’application d’un algorithme, perçu par A, sur une entrée, perçue par E. L’application dans Haskell correspond, si elle est explicitement mentionnée, à la fonction $ utilisée comme

A$E

mais nous pouvons aussi employer la notation plus courte, de la même façon que dans le Lambda calcul. Cette notation omet la fonction d’application explicite et est utilisée comme :

A E

1 Comme langages impératifs nous pouvons citer Basic, Pascal, C, C++, Java, et d’autres.

La deuxième opération de base du Lambda calcul est l’abstraction. Si M = M[x] est une expression qui contient x (ou autrement dit dépend de x), puis λx.M[x] désigne la fonction x 7→ M[x]. Par exemple, λx.x ∗ x définit la puissance de 2. Dans Haskell, nous avons également la possibilité de définir une fonction par le terme λ comme :

λx→x ∗x

Cette expression définit une nouvelle fonction sur place. Elle peut être utilisée dans les définitions des fonctions dans Haskell, comme nous pourrons le voir par la suite, par exemple, dans la définition de la fonction $, page 62.

Une des spécialités du Lambda calcul est la façon d’exprimer les fonctions de plusieurs arguments par itération de l’application1 et nous parlons ainsi d’une succession des applications partiellesWik06c.

Ainsi, si f(x, y) dépend de deux arguments x et y, nous pouvons définir dans le Lambda calcul Fx =

λy.f (x, y) qui ne dépend que de y et F = λx.Fxqui ne dépend que de x et nous obtenons par conséquent

(F x)y = Fxy = f (x, y). Haskell suit cette logique et les fonctions de plusieurs arguments sont définies

exactement de cette manière même si la syntaxe du Haskell le cache quelquefois. Par exempleHPF99, la

fonction add d’addition de deux arguments est définie dans Haskell comme :

add (x ,y) = x +y

ce qui est un exemple d’une définition uncurried. Cette définition est équivalente à une définition curried :

add x y = x +y

qui correspond à

add = λ x y → x +y

ce qui est une notation propre à Haskell mais qui n’exprime rien d’autre que

add = λ x → λy → x +y

La dernière définition correspond dans la notation du Lambda calcul à l’expression λx.λy.x + y qui peut être récrite comme λx.(λy.x + y) = λx.Fxen utilisant les fonctions partielles comme décrit auparavant.

Les fonctions standards curry et uncurry du Haskell sont dédiées à ce travail.

Avant de passer à nos propres définitions et expressions dans Haskell, nous voudrions introduire certaines notions de base pour que le lecteur non familier avec Haskell ou avec d’autres langages fonc- tionnels puisse poursuivre notre raisonnement et comprendre la syntaxe et la façon de construire les algorithmes décrits par la suite. Pour plus d’informations, nous orientons le lecteur vers une brève introduction du Haskell98HPF99.

La fonction de l’identité polymorphe id est un exemple trivial d’une fonction définie par Haskell. Elle va nous servir pour expliquer la syntaxe de ce langage :

id :: α → α

id x = x

La première ligne définit, par le symbole ::, la signature du type de la fonction id pour ses paramètres et la valeur de retour. Nous lisons la définition de gauche à droite, dans le sens des flèches. Dans ce cas, il s’agit d’une fonction qui prend un paramètre d’un type polymorphe α, et transforme sa valeur en une autre valeur du même type α.

La deuxième ligne définit, en utilisant le symbole =, le corps de la fonction. Notons que les langages basés sur le Lambda calcul définissent les fonctions comme les règles de réécriture. En définissant une fonction dans Haskell, nous prescrivons, en effet, cette règle de réécriture. L’expression à gauche de = qui correspond à la signature de la fonction sera récrite par l’expression à droite de =.

Dans ce cas précis de la fonction id, nous avons à gauche = la signature de la fonction avec un paramètre x qui sera récrite par l’expression à droite de =, c’est-à-dire en x sans le changer. C’est la définition d’identité valable pour les valeurs de n’importe quel type.

1 Il s’agit d’une idée due à M. Schönfinkel, appelée également curryficationWik06c(du terme anglais currying), selon le nom

La code source de l’identité polymorphe en Haskell est le suivant :

id :: a -> a id x = x

Le côté pratique de notre approche réside dans la possibilité de vérifier automatiquement la syntaxe et le fonctionnement des définitions en utilisant le compilateur GHCGHCdu Haskell, car toutes les définitions

sont également les fonctions définies dans ce langage. 4.2.2 Fonctions de base du Haskell

Les définitions des fonctions que nous décrivons à cette place appartiennent à la base de la program- mation fonctionnelle et sont incluses dans Haskell. Vu qu’elles se trouvent abondamment employées dans nos prochaines descriptions, nous tenons à présenter au lecteur leurs définitions exactes.

: est le constructeur d’une liste qui travaille avec un élément et une autre liste comme les arguments. Par exemple, l’expression a : [] définit la liste [a] avec un seul élément a. Les listes contenant plusieurs éléments peuvent être construites par l’applications successives de ces constructeurs, e.g. a : b : c : [] construit la liste [a, b, c].

$ est un opérateur d’application qui applique une fonction sur un paramètre. Il est utilisé dans une écriture dense ou là où l’on veut explicitement insister sur l’application de la fonction sur un paramètre :

( $ ) :: (α → β) → α → β

f $ = λx → f (x )

◦ est un infixe de composition, il crée à partir de deux fonctions une seule fonction composée :

( ◦ ) :: (β → γ) → (α → β) → α → γ

f ◦ g = λx → f (g(x ) )

\\ est un infixe de différence de deux listes, e.g. l’expression [1, 2, 3, 4]\\[2, 4] donne comme résultat une

liste [1, 3].

++ est un infixe de la concaténation de deux listes, e.g. l’expression [1, 2]++[3, 4] donne comme résultat une liste [1, 2, 3, 4].

map applique une fonction à chaque élément d’une liste. Nous pouvons y percevoir le calcul sur les streams et faire ainsi un lien avec le kernel d’application que nous avons décrit dans 3.4.1.1, page 45. Sa définition utilise la récursion :

map :: (α → β) → [ α ] → [ β]

map f [ ] = [ ]

map f (x :xs) = f x : (map f xs)

foldr1, foldl1 La fonction foldr1 prend les deux derniers éléments d’une liste et applique sur eux une fonction. Puis elle applique cette même fonction entre le résultat obtenu et le troisième élément à partir de la fin de la liste. Et ainsi de suite juqsu’à ce que tous les éléments soit traités. Nous pouvons y percevoir le calcul sur les streams et faire ainsi un lien avec le kernel de réduction que nous avons décrit dans 3.4.1.2, page 45 :

foldr1 :: (α → α → α) → [ α ] → α

foldr1 f [ x ] = x

foldr1 f (x :xs) = f x ( foldr1 f xs)

Une autre fonction, foldl1, travaille semblablement mais elle commence le traitement à partir du début de la liste et progresse vers l’arrière.

foldr, foldl La fonction foldr est une autre fonction de réduction d’un stream. Se différenciant de la précédente, foldr1, elle utilise une valeur d’entrée d’un autre type que celui du stream. Ainsi, elle permet d’effectuer la réduction sur un stream d’une manière plus générale. Elle commence par l’application de la fonction f sur la valeur d’entrée et réutilise le résultat itérativement sur tous les autres éléments de la liste.

foldr :: (α → β → β) → β → [ α ] → β

foldr f z [ ] = z

foldr f z (x :xs) = f x ( foldr f z xs)

L’utilité de cette fonction est évidente. Un exemple que nous pouvons donner pour illustrer les cas d’utilisation de cette fonction peut être le calcul d’un histogramme où nous voulons obtenir, à partir d’une liste des éléments triviaux, tels que des numéros, une structure plus complexe, tel que l’histogramme.

La fonction foldl travaille pareillement, mais elle exécute la réduction à partir de la fin de la liste et progresse vers l’avant.

filter retourne la liste avec les éléments qui satisfont le critère p. Nous pouvons y percevoir le calcul sur les streams et faire ainsi un lien avec le kernel de filtrage que nous avons décrit dans 3.4.1.3, page 45 :

filter :: (α → Bool) → [ α ] → [ α ]

filter p [ ] = [ ]

filter p (x :xs) | p x = x : filter p xs

| otherwise = filter p xs

zip crée, à partir de deux listes, une liste des tuples où chacun des tuples contient, respectivement, un élément de la première liste et un élément de la deuxième, e.g. zip [1, 2, 3] [4, 5, 6] a pour résultat [(1, 4), (2, 5), (3, 6)] :

zip :: [ α ] → [ β] → [ (α,β)]

zip (x :xs) (y:ys) = (x ,y) : zip xs ys

zip _ _ = [ ]

fst retourne le premier élément d’un tuple :

fst :: (α,β) → α

fst (x ,_) = x

snd retourne le deuxième élément d’un tuple :

snd :: (α,β) → β

snd(_,y) = y