- Accueil
- EXERCICES
- Haskell_Exercices
- Exercices Corriges 01 : Haskell
Exercices corrigés 05 : Haskell avancé
Exercices corrigés 05 : Haskell avancé
Une série progressive pour consolider Haskell puis aller plus loin : polymorphisme, contraintes de classes, évaluation paresseuse, types algébriques, récursion structurale, arbres, trie, polynômes et monades appliquées à un entraînement simplifié et à un agent de décision. Essaie chaque question avant d’ouvrir son corrigé.
Questions de cours
Répondez d’abord sans ouvrir les volets de correction.
- Donnez les types de
map,filteretfoldr. Expliquez en quoi un pliage remplace la structure récursive d’une liste. - Quelle contrainte de classe faut-il pour compter les occurrences d’une valeur? Et pour trier une liste? Expliquez les rôles de
EqetOrd. - Quelle différence y a-t-il entre un constructeur de type et une valeur construite? Illustrez avec
Maybe aetJust. - Que représentent
pureet(>>=)dans une monade? Énoncez les trois lois monadiques. - Que fait la notation
do? À quel enchaînement de(>>=)correspond-elle? - Choisissez entre
Maybe,StateetWriterpour modéliser chacun de ces besoins : calcul susceptible d’échouer, mise à jour de poids, journalisation d’une décision. - Quelle différence pratique y a-t-il entre
mapMetmapM_? - Haskell est paresseux. Que signifie cette propriété pour une fonction qui n’utilise pas l’un de ses arguments?
Afficher le corrigé
map :: (a -> b) -> [a] -> [b];filter :: (a -> Bool) -> [a] -> [a];foldr :: (a -> b -> b) -> b -> [a] -> b.foldrremplace[]par la valeur initiale et chaque(:)par la fonction de combinaison.Eq afournit l’égalité, nécessaire à(==);Ord afournit l’ordre, nécessaire à comparer les éléments d’un tri.Maybeest un constructeur de type qui prend un typeaet produitMaybe a.JustetNothingsont des constructeurs de valeurs :Just 3 :: Maybe Int,Nothing :: Maybe a.pure :: a -> m aplace une valeur dans un contexte;(>>=) :: m a -> (a -> m b) -> m benchaîne deux calculs qui partagent ce contexte. Lois :pure a >>= k = k a;m >>= pure = m;(m >>= k) >>= h = m >>= (\x -> k x >>= h).- Chaque ligne avec une flèche gauche lie le résultat d’un calcul monadique. Par exemple,
x <- actiondevientaction >>= (\x -> suite). La dernière ligne est l’expression finale. Maybesignale une absence ou un échec;Statefait évoluer un état;Writeraccumule une trace.mapMexécute une action monadique pour chaque élément et conserve les résultats dans le contexte;mapM_les ignore et conserve seulement l’effet.- Un argument inutilisé n’a pas besoin d’être évalué. La paresse permet donc, par exemple, de choisir une branche sans calculer l’autre. Haskell utilise l’évaluation paresseuse avec partage (call-by-need).
Exercice 1 — Types, contraintes et évaluation
Considérez ces définitions :
absMax [] = -1
absMax (x:xs) = max (abs x) (absMax xs)
combine f g [] = -1
combine f g (x:xs) = g (f x) (combine f g xs)
omega = omega + 1
choose True x y = x
choose False x y = y
e1 = (\x y -> x + y) (1 + 2) (3 + 4)
e2 = (\x y -> y) omega 33
e3 = choose True 0 omega- Donnez les types les plus généraux de
absMaxetcombine, avec leurs contraintes. - Donnez les types de
ordetchr, puis expliquez pourquoicount x xsimpose une contrainteEq. - Calculez
e1,e2ete3avec l’évaluation paresseuse. Que se passerait-il avec une stratégie stricte qui évalue tous les arguments avant l’appel?
Afficher le corrigé
Pour absMax, abs requiert Num a et max requiert Ord a :
absMax :: (Num a, Ord a) => [a] -> aLe cas vide fournit le neutre choisi -1; cette définition n’est pas une valeur absolue maximale conventionnelle pour une liste vide, mais respecte l’énoncé.
Dans combine, f transforme chaque élément en résultat b, et g combine deux résultats. Le littéral -1 impose Num b :
combine :: Num b => (a -> b) -> (b -> b -> b) -> [a] -> bord :: Char -> Int et chr :: Int -> Char. Une version polymorphe du compteur est :
count :: Eq a => a -> [a] -> Int
count _ [] = 0
count target (x:xs) =
(if target == x then 1 else 0) + count target xse1 = 10. Avec la paresse, e2 = 33 et e3 = 0, car omega n’est jamais demandé. Une stratégie stricte tenterait d’évaluer omega avant l’appel et ne terminerait pas pour e2 et e3.
Exercice 2 — Combinateurs de listes
- Écrivez
mapRec,filterRecetfoldrRecpar récursion structurale. - Écrivez un compteur polymorphe, une fonction
maximumSafequi renvoieNothingsur une liste vide, et une fonctionreverseFastutilisantfoldl'. - Écrivez
zipRec, qui associe les éléments par position et s’arrête à la fin de la liste la plus courte. - Définissez
repeatApply n f x, puis utilisez-la pour définir la multiplication entière par additions répétées et la puissance pour un exposant naturel. - Définissez
isPythagoreanetsameSign, en donnant des signatures qui expriment leurs contraintes.
Afficher le corrigé
import Data.List (foldl')
mapRec :: (a -> b) -> [a] -> [b]
mapRec _ [] = []
mapRec f (x:xs) = f x : mapRec f xs
filterRec :: (a -> Bool) -> [a] -> [a]
filterRec p = foldr step []
where
step x rest
| p x = x : rest
| otherwise = rest
foldrRec :: (a -> b -> b) -> b -> [a] -> b
foldrRec _ z [] = z
foldrRec f z (x:xs) = f x (foldrRec f z xs)
count :: Eq a => a -> [a] -> Int
count target = foldr (\x n -> if x == target then n + 1 else n) 0
maximumSafe :: Ord a => [a] -> Maybe a
maximumSafe = foldr step Nothing
where
step x Nothing = Just x
step x (Just y) = Just (max x y)
reverseFast :: [a] -> [a]
reverseFast = foldl' (flip (:)) []
zipRec :: [a] -> [b] -> [(a,b)]
zipRec (x:xs) (y:ys) = (x,y) : zipRec xs ys
zipRec _ _ = []
repeatApply :: Int -> (a -> a) -> a -> a
repeatApply n f x
| n <= 0 = x
| otherwise = repeatApply (n - 1) f (f x)
multiply :: Int -> Int -> Int
multiply x y
| y < 0 = negate (multiply x (negate y))
| otherwise = repeatApply y (+ x) 0
powerNat :: Int -> Int -> Int
powerNat _ n | n < 0 = error "exposant négatif"
powerNat x n = repeatApply n (* x) 1
isPythagorean :: Integral a => a -> a -> a -> Bool
isPythagorean a b c =
a > 0 && b > 0 && c > 0 && a*a + b*b == c*c
sameSign :: (Num a, Ord a) => a -> a -> Bool
sameSign a b = (a >= 0 && b >= 0) || (a < 0 && b < 0)Remarques. powerNat suppose un exposant positif ou nul. La fonction renvoie Nothing pour un exposant négatif et Just résultat sinon. Le comptage nécessite Eq, alors que les fonctions arithmétiques nécessitent Num; les comparaisons d’ordre demandent Ord.
Exercice 3 — Modéliser un robot jardinier
Un robot se déplace sur une grille infinie. Il peut transporter des plantes ou des outils, mais pas un robinet. Une case contient un robinet, un objet ou rien. Les plantes sont des légumes, des fleurs colorées ou des arbres fruitiers. Les outils sont des bêches de trois tailles, des pioches ou des arrosoirs vides ou contenant un volume d’eau. Un panier est vide, contient un légume ou deux légumes.
- Définissez des types algébriques pour les directions cardinales et relatives, les coordonnées, les plantes, outils, objets, cases, chargement du robot, état du jardin, état du robot et actions.
- Écrivez la fonction de rotation de 90 degrés.
- Écrivez
move, qui déplace une coordonnée d’une distance non négative dans une direction. - Représentez le jardin par une
Mapet écrivez une fonction pure qui ajoute ou remplace le contenu d’une case.
Afficher le corrigé
import qualified Data.Map.Strict as Map
data Cardinal = North | East | South | West
deriving (Eq, Ord, Show)
data Turn = Left90 | Right90
deriving (Eq, Show)
data Coord = Coord Int Int
deriving (Eq, Ord, Show)
turn :: Turn -> Cardinal -> Cardinal
turn Left90 North = West
turn Left90 West = South
turn Left90 South = East
turn Left90 East = North
turn Right90 North = East
turn Right90 East = South
turn Right90 South = West
turn Right90 West = North
move :: Cardinal -> Int -> Coord -> Coord
move direction distance (Coord x y) =
let d = max 0 distance
in case direction of
North -> Coord x (y + d)
East -> Coord (x + d) y
South -> Coord x (y - d)
West -> Coord (x - d) y
data Vegetable = Bean | Carrot | Squash
deriving (Eq, Ord, Show)
data Colour = Red | Yellow | Blue
deriving (Eq, Ord, Show)
data Fruit = Apple | Pear | Plum
deriving (Eq, Ord, Show)
data Plant
= VegetablePlant Vegetable
| Flower Colour
| FruitTree Fruit
deriving (Eq, Show)
data SpadeSize = Small | Medium | Large
deriving (Eq, Show)
data Water = EmptyCan | Litres Int
deriving (Eq, Show)
data Tool = Spade SpadeSize | Pickaxe | WateringCan Water
deriving (Eq, Show)
data Basket = EmptyBasket | OneVegetable Vegetable | TwoVegetables Vegetable Vegetable
deriving (Eq, Show)
data Object = PlantObject Plant | ToolObject Tool | BasketObject Basket
deriving (Eq, Show)
data Cell = Tap | Contains Object | Vacant
deriving (Eq, Show)
data Load = NothingLoaded | Carrying Object
deriving (Eq, Show)
type Garden = Map.Map Coord Cell
data Robot = Robot
{ position :: Coord
, direction :: Cardinal
, load :: Load
} deriving (Eq, Show)
data Action = MoveBy Int | TurnBy Turn | PickUp | PutDown | WaterBy Int
deriving (Eq, Show)
place :: Coord -> Cell -> Garden -> Garden
place = Map.insertExemples : Flower Red, Spade Large, WateringCan (Litres 3), BasketObject (Two Vegetables Bean Carrot). Le type Load ne possède volontairement pas de constructeur pour Tap : le robot ne peut donc pas transporter un robinet.
Exercice 4 — Plis d’un arbre récursif
On considère un arbre où chaque feuille porte un entier et chaque nœud interne contient une liste ordonnée d’enfants :
data Arbre = Feuille Int | Noeud [Arbre]
deriving (Eq, Show)- Définissez le type d’algèbre et le pli
foldArbre. - Définissez des algèbres qui calculent la somme des feuilles, la hauteur (feuille de hauteur 1), le parcours infixe des feuilles et un affichage parenthésé.
- Calculez ces résultats pour l’arbre
Noeud [Noeud [Feuille 4, Feuille 5], Feuille 2, Feuille 3]. - Extension. Ajoutez
Implique Formule Formuleà un langage de formules booléennes fondé sur les constructeurs Vrai, Faux, Non, Et et Ou. Quelles définitions doivent être modifiées?
Afficher le corrigé
import Data.List (intercalate)
data ArbreAlg a = ArbreAlg
{ feuilleAlg :: Int -> a
, noeudAlg :: [a] -> a
}
foldArbre :: ArbreAlg a -> Arbre -> a
foldArbre alg (Feuille n) = feuilleAlg alg n
foldArbre alg (Noeud enfants) =
noeudAlg alg (map (foldArbre alg) enfants)
sommeAlg :: ArbreAlg Int
sommeAlg = ArbreAlg id sum
hauteurAlg :: ArbreAlg Int
hauteurAlg = ArbreAlg (const 1) (\hs -> 1 + maximum (0 : hs))
listeAlg :: ArbreAlg [Int]
listeAlg = ArbreAlg (:[]) concat
renduAlg :: ArbreAlg String
renduAlg = ArbreAlg show (\xs -> "(" ++ intercalate "," xs ++ ")")
arbreExemple :: Arbre
arbreExemple =
Noeud [Noeud [Feuille 4, Feuille 5], Feuille 2, Feuille 3]Résultats : somme 14, hauteur 3, parcours [4,5,2,3]. L’affichage donne une représentation imbriquée qui conserve l’ordre des enfants.
Pour l’extension, il faut ajouter le constructeur au type Formule, au type d’algèbre, au pli, puis à chaque algèbre (évaluation, affichage, etc.). Pour l’évaluation booléenne, implique p q = not p || q.
Exercice 5 — Un dictionnaire préfixé avec un trie
Le dictionnaire contient les mots art, article, artifice, balle, ballon, la, langage et langue. Représentez-le par un arbre de préfixes : chaque arête porte une lettre et chaque nœud indique si un mot se termine à cet endroit.
- Définissez un type de trie qui évite d’avoir deux enfants pour la même lettre.
- Écrivez l’insertion, la recherche exacte d’un mot et la récupération des mots partageant un préfixe.
- Construisez le dictionnaire de l’énoncé et donnez les résultats pour
memberWord "art",memberWord "arti"et les mots de préfixe"art".
Afficher le corrigé
import qualified Data.Map.Strict as Map
import Data.List (foldl')
data Trie = Trie
{ isWord :: Bool
, children :: Map.Map Char Trie
} deriving (Eq, Show)
emptyTrie :: Trie
emptyTrie = Trie False Map.empty
insertWord :: String -> Trie -> Trie
insertWord [] (Trie _ kids) = Trie True kids
insertWord (c:cs) (Trie end kids) =
let child = Map.findWithDefault emptyTrie c kids
child' = insertWord cs child
in Trie end (Map.insert c child' kids)
memberWord :: String -> Trie -> Bool
memberWord [] (Trie end _) = end
memberWord (c:cs) (Trie _ kids) =
maybe False (memberWord cs) (Map.lookup c kids)
insertAll :: [String] -> Trie
insertAll = foldl' (flip insertWord) emptyTrie
allWords :: Trie -> [String]
allWords (Trie end kids) =
(if end then [""] else []) ++
[ c : suffix
| (c, child) <- Map.toList kids
, suffix <- allWords child
]
followPrefix :: String -> Trie -> Maybe Trie
followPrefix [] trie = Just trie
followPrefix (c:cs) (Trie _ kids) =
Map.lookup c kids >>= followPrefix cs
wordsWithPrefix :: String -> Trie -> [String]
wordsWithPrefix prefix trie =
maybe [] (map (prefix ++) . allWords)
(followPrefix prefix trie)
dictionary :: Trie
dictionary = insertAll
["art", "article", "artifice", "balle", "ballon",
"la", "langage", "langue"]memberWord "art" dictionary vaut True; memberWord "arti" dictionary vaut False, car « arti » n’est pas marqué comme mot même s’il est un préfixe. wordsWithPrefix "art" dictionary renvoie ["art","article","artifice"] (l’ordre vient de Map.toList).
Exercice 6 — Polynômes et évaluation par pli
Représentez un polynôme par la liste de ses coefficients, du terme constant vers le degré le plus élevé. Par exemple, Poly [1,2,3] représente 1 + 2x + 3x².
- Définissez
Poly, son évaluation et sa dérivée. - Expliquez pourquoi la formule d’évaluation par Horner est un pli sur les coefficients.
- Calculez le résultat de
evalPoly (Poly [1,2,3]) 2.
Afficher le corrigé
newtype Poly a = Poly [a]
deriving (Eq, Show)
evalPoly :: Num a => Poly a -> a -> a
evalPoly (Poly coefficients) x =
foldr (\coefficient acc -> coefficient + x * acc)
0 coefficients
differentiate :: Num a => Poly a -> Poly a
differentiate (Poly []) = Poly []
differentiate (Poly (_:coefficients)) =
Poly (zipWith (\degree coefficient ->
fromIntegral degree * coefficient)
([1..] :: [Int]) coefficients)Horner développe c₀ + x(c₁ + x(c₂ + …)), ce qui correspond directement à foldr. Le test vaut 1 + 2·2 + 3·2² = 17. La dérivée de [c₀,c₁,c₂,…] est représentée par [c₁,2c₂,3c₃,…].
Exercice 7 — State : mises à jour séquentielles de poids
Simulez l’entraînement d’un modèle. Chaque taux fourni représente une correction à soustraire à chacun des poids. Écrivez une mise à jour pure, une étape monadique updateStep, puis runOptimization, qui exécute les étapes dans l’ordre.
Test : runOptimization [0.1, 0.05] [1.0, 2.0] doit produire [0.85, 1.85].
Afficher le corrigé
import Control.Monad.State.Strict
(State, execState, get, put)
type Weights = [Double]
type Optimizer a = State Weights a
mockGradientUpdate :: Double -> Weights -> Weights
mockGradientUpdate delta = map (\weight -> weight - delta)
updateStep :: Double -> Optimizer ()
updateStep delta = do
current <- get
put (mockGradientUpdate delta current)
runOptimization :: [Double] -> Weights -> Weights
runOptimization deltas initial =
execState (mapM_ updateStep deltas) initialLa première étape transforme les poids en [0.9,1.9]; la suivante donne [0.85,1.85]. mapM_ enchaîne les actions en conservant l’état final, renvoyé par execState. Il s’agit d’une simulation pédagogique, pas d’un calcul réel de gradient.
Exercice 8 — Writer : journaliser les décisions d’un agent
Un agent attribue une ressource A, B ou C selon trois scores. Il doit renvoyer le choix et enregistrer un message à chaque décision.
- Définissez les types
ResourceScore,DecisionLogetDecisionAgent. - Écrivez
logDecision,allocateResourceetsequentialDecisions. - Testez avec les scores
(10,5,2)et(3,8,1).
Afficher le corrigé
import Control.Monad.Writer.Strict
(Writer, runWriter, tell)
import Data.List (maximumBy)
import Data.Ord (comparing)
type ResourceScore = (Double, Double, Double)
type DecisionLog = [String]
type DecisionAgent a = Writer DecisionLog a
logDecision :: String -> DecisionAgent ()
logDecision message = tell [message]
allocateResource :: ResourceScore -> DecisionAgent String
allocateResource (scoreA, scoreB, scoreC) = do
let (name, score) =
maximumBy (comparing snd)
[("A", scoreA), ("B", scoreB), ("C", scoreC)]
logDecision
("Allocation faite à la ressource " ++ name
++ " avec le score " ++ show score)
pure name
sequentialDecisions
:: ResourceScore -> ResourceScore
-> DecisionAgent (String, String)
sequentialDecisions first second = do
a <- allocateResource first
b <- allocateResource second
pure (a, b)
runDecisions
:: ResourceScore -> ResourceScore
-> ((String, String), DecisionLog)
runDecisions first second =
runWriter (sequentialDecisions first second)runDecisions (10,5,2) (3,8,1) renvoie (("A","B"), [messageA,messageB]). tell [message] ajoute un élément au journal; la monade Writer concatène les listes dans l’ordre des actions. Les scores d’exemple n’ont pas d’égalité, donc la règle de départage n’intervient pas.
Exercice 9 — Défi avancé : StateT et erreurs
Étendez l’optimiseur pour qu’il refuse un gradient de longueur différente du vecteur de poids. Le calcul doit retourner soit une erreur lisible, soit les poids finaux. Une étape valide applique w - taux × gradient.
- Définissez
SafeOptimizercomme unStateTsurEither String. - Écrivez
applyBatchavec une vérification de longueur. - Écrivez
trainSafeet testez un cas valide et un cas invalide.
Afficher le corrigé
import Control.Monad.State.Strict
(StateT, execStateT, get, put)
import Control.Monad.Trans.Class (lift)
type Weights = [Double]
type SafeOptimizer a = StateT Weights (Either String) a
applyBatch :: Double -> [Double] -> SafeOptimizer ()
applyBatch rate gradient = do
weights <- get
if length weights /= length gradient
then lift (Left "Le gradient et les poids n'ont pas la même taille.")
else put (zipWith (\weight g -> weight - rate * g)
weights gradient)
trainSafe
:: Double -> [[Double]] -> Weights
-> Either String Weights
trainSafe rate batches initial =
execStateT (mapM_ (applyBatch rate) batches) initialtrainSafe 0.1 [[0.5,1.0],[1.0,0.0]] [1.0,2.0] donne Right [0.85,1.9]. Avec un gradient de longueur 1, l’étape renvoie Left; la suite de la séquence ne s’exécute pas. StateT s (Either e) combine donc un état et une interruption par erreur.
Questions / Réponses
Commentaires
-
1 Viviane Le 11/03/2023
Merci beaucoup c'est très facile pour l'apprentissage -
2 Junior Le 08/03/2023
Satisfaisant -
3 nanij Le 07/03/2023
Merci pour ce partage de connaissances -
4 Franck Duhamel Le 06/03/2023
Ce site est à recommander. merci -
5 steve nn Le 06/03/2023
Vraiment intéressant -
6 Ivan Le 06/03/2023
Merci pour vos solutions très intéressantes
Ajouter un commentaire