Haskell : TD 1
« Paradigmes et langages non classiques 2014
Ce TD permet de manipuler et d’approfondir les différents concepts abordés lors du premier cours Haskell.
Calculatrice RPN
Une calculatrice RPN travaille avec une pile. Les opérations (par exemple l’addition) travaillent avec les valeurs au sommet de la pile et les y remettent. Le but de cet exercice est de factoriser un maximum de code et de limiter la duplication.
Types de base
Le mot-clé “type” permet de déclarer un alias de type. Par exemple, le type chaîne est défini comme :
type String = [Char]
Définir un type Stack
comme étant équivalent à une liste d’entiers
courts (Int
). Définir un type Operator
représentant une fonction
travaillant sur la pile. Un tel opérateur prend une pile en paramètre,
utilise certaines valeurs et renvoie la nouvelle pile.
Fonctions
Définir ensuite une fonction parseOp
ayant comme signature :
parseOp :: String -> Operator
qui, lorsqu’on lui passe les valeurs "+"
, "-"
, "*"
, fait ce
qu’on attend intuitivement (la division entière en Haskell est la
fonction div
).
parseOp
doit également reconnaître les opérateurs
"dup"
, "swap"
, "drop"
qui, respectivement, duplique le sommet de la
pile, échange les deux valeurs au sommet de la pile, efface l’élement
au sommet de la pile. "depth"
renvoie le nombre d’éléments dans la
pile au moment de l’appel, "pick"
renvoie le n-ième élément de la
pile.
De plus, si on passe une chaîne contenant un entier à parseOp
, l’entier
correspondant doit être ajouté au sommet de la pile. Pour cela, on pourra
utiliser la fonction read
qui permet, entre autres, de transformer une
chaîne en une entité pour tous les objets de la classe de types Read
.
Faire ensuite une fonction eval
qui prend une pile et une liste
[Operator]
et appliquent les opérateurs à la suite. La signature
d’eval
sera donc:
eval :: Stack -> [Operator] -> Stack
Le lecteur astucieux remarquera qu’en fait, eval
n’est pas spécifique
aux piles, mais consiste à appliquer à une donnée initiale tous les opérateurs
présents dans une liste et à renvoyer la donnée finale, et que la signature
peut donc être dé-spécialisée en :
eval :: a -> [a -> a] -> a
En utilisant Hoogle, le moteur de
recherche spécialisé Haskell, on trouvera le module à importer pour
utiliser hFlush
. De la même manière, on trouvera probablement une
fonction existante permettant de séparer une chaîne de caractères en
ses différents composants, ce qui sera utile pour implémenter parse
dont la signature est à deviner. On pourra tester les opérations à
l’aide du programme principal suivant :
repl :: Stack -> IO ()
= do
repl stack putStr "> "
hFlush stdout<- getLine
line <- return $ eval stack (parse line)
newstack putStrLn $ show $ reverse newstack
repl newstack= repl [] main
Nombres de Peano
Les nombres de Peano permettent de représenter les entiers
non-négatifs. 0 est défini comme Zero
, 1 comme le successeur de Zero
,
2 comme le successeur du successeur de Zero
, etc. Définir, avec
data
, un type Peano
comme étant un type avec deux constructeurs,
Zero
(qui ne prend pas d’argument) et Succ
(qui prend un nombre de
Peano). On souhaite que le type Peano
puisse être une instance de la
classe Num
. Pour cela, il faut définir les opérations suivantes:
+
, -
, *
, signum
(qui renvoie 0, 1 ou -1 selon le signe du
nombre passé en argument, ou plutôt Zero
ou Succ Zero
vu qu’on
n’a pas de nombre négatif), abs
et fromInteger
. La déclaration
d’instance ressemblera donc à:
instance Num Peano where
+ b = ...
a - b = ... a
On souhaite également que les nombres de Peano soient regardables : on
les instancie donc avec la classe Show
et on implémentera une
méthode show
.
Retour sur l’interpréteur RPN
Modifier l’interpréteur RPN pour qu’il utilise les nombres de Peano
définis ci-dessus. On instanciera aussi la classe Read
pour pouvoir
saisir des nombres de Peano plus facilement.
Modifier ensuite l’interpréteur RPN afin qu’il utilise n’importe quel
type qui appartient aux classes de types Integral
et Read
(Show
sera aussi nécessaire pour pouvoir implémenter l’interpréteur).
Annexe
Il est possible de tester, en installant le paquetage Haskell QuickCheck, les nombres de Peano (s’ils sont dans un module de même nom) avec le code suivant :
module CheckPeano where
import Control.Monad
import Peano
import Test.QuickCheck
instance Arbitrary Peano where
= liftM toEnum $ choose (0, 10)
arbitrary Succ a) = [a]
shrink (Zero = []
shrink
-- Run only if you accept "S(S(Z))" as an input
= read (show p) == p
prop_showread p where types = p :: Peano
-- Run only if you accept "SSZ" as an input
= read (filternot (`elem` "()") $ show p) == p
prop_showread_noparen p where types = p :: Peano
= filter (\x -> not $ p x)
filternot p
= p == p
prop_eq p where types = p :: Peano
= p /= Succ(p)
prop_neq p where types = p :: Peano
= (toInteger p1 > toInteger p2) == (p1 > p2)
prop_gt p1 p2 where types = p1 :: Peano
= p1 + p2 == p2 + p1
prop_comm_add p1 p2 where types = p1 :: Peano
= p1 * p2 == p2 * p1
prop_comm_mul p1 p2 where types = p1 :: Peano
= (p1 + p2) - p1 == p2
prop_add_sub p1 p2 where types = p1 :: Peano
= p1 /= Zero ==> (p1 * p2) `div` p1 == p2
prop_mul_div p1 p2 where types = p1 :: Peano
= p == abs(p)
prop_abs p where types = p :: Peano
= signum p == if p == Zero then Zero else Succ Zero
prop_signum p where types = p :: Peano
= (toInteger p) + 1 == (toInteger (Succ p))
prop_to_integer p where types = p :: Peano
= i >= 0 ==> Succ (fromInteger i) == fromInteger (i+1)
prop_from_integer i where types = i :: Integer
= do
main
quickCheck prop_showread
quickCheck prop_showread_noparen
quickCheck prop_eq
quickCheck prop_neq
quickCheck prop_gt
quickCheck prop_comm_add
quickCheck prop_comm_mul
quickCheck prop_add_sub
quickCheck prop_mul_div
quickCheck prop_abs
quickCheck prop_signum
quickCheck prop_to_integer quickCheck prop_from_integer
Pour tester la calculatrice RPN, on pourra utiliser :
module CheckRPN where
import RPN
import Data.List
import Test.QuickCheck
= length s > 0 ==> case s of (x : _) -> parseOp "dup" s == x : s
prop_dup s where types = s :: [Integer]
= length s > 0 ==> parseOp "drop" s == tail s
prop_drop s where types = s :: [Integer]
= length s >= 2 ==> case s of (a : b : xs ) -> parseOp "swap" s == b : a : xs
prop_swap s where types = s :: [Integer]
= parseOp "depth" s == (fromIntegral $ length s) : s
prop_depth s where types = s :: [Integer]
= length s >= 2 && (fromIntegral (head s) < length s - 1) && head s >= 0 ==>
prop_pick s case s of (d : xs) -> parseOp "pick" s == (genericIndex xs d) : xs
where types = s :: [Integer]
= length s >= 2 ==> case s of (b : a : xs) -> parseOp "+" s == (a+b) : xs
prop_add s where types = s :: [Integer]
= length s >= 2 && fromIntegral (s !! 1) >= fromIntegral (s !! 0) ==>
prop_sub s case s of (b : a : xs) -> parseOp "-" s == (a-b) : xs
where types = s :: [Integer]
= length s >= 2 ==> case s of (b : a : xs) -> parseOp "*" s == (a*b) : xs
prop_mul s where types = s :: [Integer]
= length s >= 2 && fromIntegral (s !! 0) /= 0 ==>
prop_div s case s of (b : a : xs) -> parseOp "/" s == (a `div` b) : xs
where types = s :: [Integer]
= do
main
quickCheck prop_dup
quickCheck prop_drop
quickCheck prop_swap
quickCheck prop_depth
quickCheck prop_pick
quickCheck prop_add
quickCheck prop_sub
quickCheck prop_mul quickCheck prop_div