Haskell est un langage fonctionnel pur. Pas de constructions impératives (commandes, instructions). Le programme est composé de définitions de fonctions et de structures de données, de directives (importation, options), et d'une seule expression qui est évaluée, et donne le résultat final.
Une variable affectée, par ex. x=2*17
devient synonymique avec sa valeur, on ne peut plus la changer! Pas d'effets de bord. Pas de boucles for
, or while, seulement la récursion. (Et même ça est redondant...)
Exceptions :
ad3 = (3 +)
, équivalente à ad3 y = 3 + y
, ou : ad3 = \y -> 3+y
.
-- (\ est "lambda" de Church.)
Le 3 a été "caché" à l'intérieur de la fonction, en constituant une fermeture. Ainsi on peut faire en Haskell les structures de données sans structures de données (même si les listes et d'autres composites existent). Définition d'une paire de 2 objets: secret = pair "Schtroumpf" 42
, où on définit :
pair x y sel = sel x yLa variable
secret
est opaque, aucun moyen d'accéder à l'intérieur. On peut récupérer le contenu (mais jamais le modifier !) grâce au paramètre sel
– la fonction de sélection, par exemple
first a b = a second a b = b -- et l'expression suivante donne "Schtroumpf" secret first
mais il faut appliquer la fermeture, elle est ainsi "détruite" dans cette expression.
Haskell est un langage paresseux, une fonction n'évalue pas les arguments dont elle n'a pas (ou pas encore !) besoin. L'expression second (1/0) 7
ne déclenche aucune erreur.
Ce, qui est une structure de données, par ex. une liste, en Haskell est un processus, dont les items sont évalués / exécutés séquentiellement, pendant leur consommation.
Les listes en Haskell : [a,b,c,...]
peuvent être construites par l'op. (:)
,
(x : [a,b])
vaut [x,a,b]
.
uns = 1 : uns -- [1,1,1,...] suite n = n : suite (n+1) -- [n, n+1, n+2,...]mais récupérer quelques éléments est facile, par ex.
take 10 (suite 66)
, où:
take 0 _ = [] -- prédéfinie
take m (x:reste) = x:(take (m-1) reste)
Les définition paresseuses sont des équations, et le système est capable de les résoudre itérativement (pas toujours). Voici une autre définition de suite
:
zipWith f (x:xs) (y:ys) =f x y : zipWith f xs ys -- zipWith est prédéfini suite n = ls where ls = n : zipWith (+) uns lsOn peut faire des choses intéressantes...
add = zipWith (+) fs = 0 : 1 : add fs (tail fs)où
tail (a:as) = as
? Ce genre de programmes sert à construire les programmes qui bouclent: le serveurs, les systèmes d'exploitation, ou autres applications communicantes. Mais on peut faire des choses plus étonnantes. On n'a pas besoin de récursivité pour écrire les programmes récursifs. On peut construire des points fixes fonctionnels.
fun g n = if n==0 then 1 else n*g (n-1)
fun
n'est qu'un "applicateur" de g
, non pas la factorielle. Mais ajoutons la définition d'un point fixe d'une fonction:
fix f = x where x = f xou, en substituant
fix f
pour x
– fix f = f (fix f). Un langage strict (non-paresseux) bombera immédiatement si on lance fix f
. Mais ici, si f
n'a pas besoin de son argument...
Oui, fix fun
est la factorielle.
data Sexe = Homme | Femme type Age = Integer -- Synonyme data FichePers = Fiche Sexe Age String -- la chaîne : par ex. le nomPour Haskell une abstraction est un type arbitraire. Le compilateur peut déduire les dépendances entre eux. Le type de
first
est : p1 -> p2 -> p1. Haskell peut déduire aussi le type des combinateurs:
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
data Liste a = Vide | Seq a (Liste a) ... addEl x l = Seq x l -- ajout d'un élément -- Pour affichage, Haskell ne peut le deviner... instance (Show a) => Show (Liste a) where show Vide = "Vide" show (Seq x y) = show x ++ " ->> " ++ show y
On peut spécifier des structures infinies, par ex.
data Arbre a = Noeud (Arbre a) a (Arbre a)
On peut les construire sans problèmes, grâce à la paresse, mais la procédure d'affichage sera délicate. Devinez comment le faire. C'était un de mes exercices en L3 Info préférés...
cube x = x*x*x
bonne pour x
quelconque? Qu'est ce que c'est "*
"? La réponse est un peu "O-O", au sens : les données (en fait : leurs types) soumises aux opérations, spécifient elles-mêmes où chercher le sens de ces opérations.
Le type de x
doit – justement – être compatible avec la multiplication, il (le type !) doit appartenir à la "classe de nombres" (généralisés).
Si on demande le type de cube, la réponse est : cube :: Num a => a -> a .
C'est comme ça que l'on définit des concepts comme "objets imprimables" (disposant de la fonction show
qui construit une chaîne représentant l'objet pour l'oeil humain), ou "objets comparables" (égalité (==)
; ordre (<)
). Et c'est comme ça que nous pouvons définir le concept de, disons, l'"espace vectoriel" – la classe de types admettant l'addition, multiplication par nombre, etc.
Puisque la définition d'une classe, c'est la spécification des opérations qui peuvent être nombreuses, les programmes Haskell ne se réduisent pas à une ligne... Mais – pas de mots-clés structurants (begin - end, etc.).
Il y a des parenthèses, accolades et indentation. Voici le crible d'Eratosthène, calculant tous les premiers :
primes :: [Int] -- pour mémoire. primes = filterPrime [2..] -- abrév. : 2, 3, 4, 5, ... where filterPrime (p:xs) = -- param. auto-destructuré p : filterPrime [x | x <- xs, x `mod` p /= 0]
La dernière ligne contient une compréhension, une liste engendrée dynamiquement depuis une autre, avec (peut-être) un dispositif de filtrage.
Les formes A where B=...
, et aussi let A=... in B
, permettent d'inclure des variables locales, temporaires.