Programmation fonctionnelle
La documentation de GHCi est ici : https://ghc.gitlab.haskell.org/ghc/doc/users_guide/ghci.html#.
Les directives de base de GHCi commencent par : :
:help ou :? : affiche l’aide.:type expression ou :t expression :
affiche le type d’une expression.:load Fichier.hs ou :l Fichier.hs : charge
un fichier Haskell.:reload ou :r : recharge le dernier
fichier chargé.:info nom ou :i nom : affiche les
informations sur une fonction, un type ou une classe.:set option : active une option, par exemple
:set +s pour afficher le temps et la mémoire utilisés.:! commande : exécute une commande système, par exemple
:! ls.:quit ou :q : quitte GHCi.Pour appliquer un réglage à chaque démarrage de GHCi, créez ou
modifiez le fichier ~/.ghci. Par exemple :
:set prompt "\ESC[34mλ> \ESC[m"
Et voici un exemple d’utilisation.
λ> :load TP-01.hs
λ> :t map
map :: (a -> b) -> [a] -> [b]
λ> :reload
λ> :quitPour chacune des expressions suivantes, déterminer sa valeur et proposer un type explicite correspondant, puis vérifier avec l’interpréteur.
21 + 2(+) 21 221 + 2 * 3(+) 21 (2*3)(+) 21 ((*) 2 3)1 == 21 /= 21 + (-1)div 11 211 `div` 242 `mod` 2 == 0even 42odd 42pred 42sqrt 49Pour vérifier le type, vous pouvez essayer d’ajouter une annotation de type à l’expression pour vous assurer que ça ne cause pas d’erreur. Par exemple :
λ> 11 / 2 :: Double
5.5
λ> 11 / 2 :: Int
<interactive>:41:1: error:
• No instance for (Fractional Int) arising from a use of ‘/’
...La structure de contrôle conditionnelle en Haskell possède la syntaxe suivante :
if expr1 then expr2 else expr3Attention, il s’agit d’une expression, elle a donc
un type et une valeur. L’expression expr1 est de type
Bool et les expressions expr2 et
expr3 doivent être du même type (qui est également le type
de l’expression complète).
λ> if True then "#t" else "#f"
"#t"
λ> if False then "#t" else "#f"
"#f"
λ> :type if True then "#t" else "#f"
if True then "#t" else "#f" :: Stringλ> x = ...
λ> y = ...
λ> maxValue = ...abs) :λ> x = ...
λ> absoluteValue = ...ceiling).λ> x = ...
λ> ceil = ...Indice : vous pouvez utiliser la parité de \(x\).
Une fonction Haskell se définit avec son nom, ses paramètres, puis
= et le résultat :
λ> double x = 2 * xIci, double reçoit x et renvoie
2 * x.
Par inférence de type, GHC est capable de déterminer le
type de la fonction lui-même :
λ> :t double
double :: Num a => a -> a
Mais dans ce cours, je vous demande d’ajouter la signature de type au-dessus de la définition, par exemple :
double :: Int -> Int
double x = 2 * xInt -> Int signifie : la fonction prend un
Int et renvoie un Int.
Avec plusieurs paramètres, ça donne :
addTwo :: Int -> Int -> Int
addTwo x y = x + yAvec des types différents :
isBig :: Int -> Bool
isBig x = x > 100Si vous avez essayé de tester, vous avez remarqué que l’on atteint
les limites de GHCi car on ne peut pas écrire le type puis
la définition de la fonction sur la ligne suivante. À partir de
maintenant, vous allez donc écrire toutes vos définitions de fonctions
dans un fichier. Aujourd’hui, créez le fichier TP-01.hs et
recopiez les définitions précédentes dedans. Puis dans
GHCi, vous pouvez charger les définitions et les tester
:
λ> :load TP-01.hs
λ> :t isBig
isBig :: Int -> Bool
λ> isBig 10
FalsemyAdd qui calcule
la somme de deux entiers.λ> myAdd 5 8
13myMinimum qui
calcule le minimum de deux réels.λ> myMinimum 5.0 8.1
5.0myMinimumOfThree
qui calcule le minimum de trois réels. On rappelle que la programmation
fonctionnelle repose en partie sur la composition de fonctions.λ> myMinimumOfThree 5.0 8.1 2.3
2.3Une fonction récursive est une fonction qui s’appelle elle-même. Pour l’écrire, il faut d’abord préciser exactement ce qu’elle doit calculer, puis identifier :
La question à se poser est : « Si je connais la solution pour un problème plus petit, comment puis-je en déduire la solution pour le problème initial ? » Il faut aussi vérifier que chaque appel récursif rapproche du cas de base.
Par exemple, voici la défintion de la factorielle :
fac :: Int -> Int
fac n = if n == 0 then 1 else n * fac (n - 1)+ ni -, ni
myAdd, définir (avec son type) une fonction
myAdd' qui calcule la somme de deux entiers \(x\) et \(y\). Nous supposerons dans cet exercice que
\(y \geq 0\). Rappel : en Haskell, il
existe des fonctions pour obtenir le prédécesseur ou le successeur d’un
entier.λ> myAdd' 5 8
13La suite de Syracuse est une suite d’entiers naturels définie de la manière suivante : on part d’un nombre entier strictement positif ; s’il est pair, on le divise par 2 ; s’il est impair, on le multiplie par 3 et l’on ajoute 1. En répétant l’opération, on obtient une suite d’entiers strictement positifs dont chacun ne dépend que de son prédécesseur.
nextSyracuse qui,
étant donné un entier, calcule le suivant dans la suite de
Syracuse.λ> nextSyracuse 14
7
λ> nextSyracuse 7
22syracuse qui,
étant donné un entier, renvoie le nombre d’étapes pour que la suite de
Syracuse arrive à 1 (remarque : la conjecture de Syracuse, aussi appelée
conjecture de Collatz, affirme que, quelle que soit la valeur entière
positive de départ, la suite finit toujours par atteindre 1… mais ce
n’est pas prouvé).λ> syracuse 14
17Définir, avec son type, une fonction sumDigits qui
prend un entier positif ou nul et renvoie la somme de ses
chiffres.
λ> sumDigits 2048
14
λ> sumDigits 111
3Le mot-clé where permet de définir des fonctions ou
variables locales, utilisées uniquement par la fonction qui le précède.
Par exemple, on peut écrire :
circleArea :: Double -> Double
circleArea radius = pi * squaredRadius
where
squaredRadius = radius * radius
middle :: (a, a, a) -> a
middle trio = y
where
(x, y, z) = trioLa structure est toujours :
functionName parameters = expression
where
localDefinition1 = ...
localDefinition2 = ...numberAndSumDigits
qui prend un entier positif ou nul et renvoie une paire contenant son
nombre de chiffres et la somme de ses chiffres. L’objectif de cette
question est de ne faire la décomposition récursive en chiffres qu’une
seule fois.λ> numberAndSumDigits 2048
(4,14)
λ> numberAndSumDigits 111
(3,3)isPrime qui teste
si un entier \(n\) est premier. On
rappelle qu’un nombre premier est un nombre entier naturel qui possède
exactement deux diviseurs entiers positifs distincts : 1 et lui-même. On
supposera ici que \(n\) est positif.
Souvenez-vous qu’il est souvent utile de définir des fonctions
intermédiaires. Autrement dit, posez-vous la question : “Est-ce que la
fonction isPrime est récursive (est-ce que ça sert à
quelque chose de savoir que \(n-1\) est
premier pour savoir si \(n\) est
premier…) ?λ> isPrime 6
False
λ> isPrime 7
TrueLes gardes permettent de choisir un résultat selon des conditions.
Elles s’écrivent après les paramètres d’une fonction avec
|.
absoluteValue :: Int -> Int
absoluteValue number
| number >= 0 = number
| otherwise = -numberLes gardes sont testées dans l’ordre :
grade :: Int -> String
grade score
| score >= 16 = "very good"
| score >= 10 = "passed"
| otherwise = "failed"λ> grade 18
"very good"
λ> grade 12
"passed"
λ> grade 7
"failed"La structure générale est :
functionName parameters
| condition1 = result1
| condition2 = result2
| otherwise = defaultResultNotez qu’il n’y a pas de = après les paramètres et avant les gardes.
On peut les combiner avec where :
fizzBuzz :: Int -> String
fizzBuzz number
| divisibleBy3 && divisibleBy5 = "FizzBuzz"
| divisibleBy3 = "Fizz"
| divisibleBy5 = "Buzz"
| otherwise = ""
where
divisibleBy3 = number `mod` 3 == 0
divisibleBy5 = number `mod` 5 == 0Les gardes permettent d’écrire des suites de
if-then-else imbriqués de façon lisible.
countSetBits qui prend un entier positif ou nul et compte
les chiffres 1 dans son écriture binaire.λ> countSetBits 13
3
λ> countSetBits 8
1
λ> countSetBits 0
0isPowerOfThree qui teste si un entier positif est une
puissance de 3. On considère que 1 est une
puissance de 3, car 1 = 3^0.λ> isPowerOfThree 81
True
λ> isPowerOfThree 45
False
λ> isPowerOfThree 1
True
λ> isPowerOfThree 0
FalseisPrime' avec des gardes.