drawing

Cours 02 : on commence à programmer¶

Vous avez commencé à vous familiariser avec le langage et l'interpréteur ghci et vous avez défini vos premières fonctions. Nous allons ajouter d'autres éléments pour continuer.

Structures de données et un peu de syntaxe¶

Tuples¶

Nous avons vu, au détour d'un exemple, que l'on pouvait définir des tuples assez naturellement.

In [1]:
-- commentaire : directive pour le compilateur ghc 
:opt no-lint
In [2]:
(1,2)
(1,2)
In [3]:
(1,2,3)
(1,2,3)
In [4]:
:t ('a','b')
('a','b') :: (Char, Char)
In [5]:
:t ('a',True)
('a',True) :: (Char, Bool)
In [6]:
-- ceci n'est pas un tuple
:t ('a')
('a') :: Char
In [7]:
(1,(2,3))
(1,(2,3))
In [8]:
fst ("a",'b')
"a"
In [9]:
snd ("a",'b')
'b'

fst et snd prennent une paire et renvoient respectivement sa première et sa deuxième composante.

Remarque : ces fonctions s'appliquent uniquement aux paires. Elles ne fonctionnent pas avec les triplets, les quadruplets, etc. Nous verrons un peu plus loin d’autres façons d’extraire des données d’un tuple.

Premières listes¶

In [10]:
[1,2,3]
[1,2,3]
In [11]:
:t [True,False,False]
[True,False,False] :: [Bool]
In [12]:
[]
[]
In [13]:
:t []
[] :: forall a. [a]
In [14]:
:t [[1,2],[],[1,2,3]]
[[1,2],[],[1,2,3]] :: forall a. Num a => [[a]]

On peut définir des ranges.

In [15]:
[1..10]
[1,2,3,4,5,6,7,8,9,10]
In [16]:
[10..1]
[]
In [17]:
[10,9..1]
[10,9,8,7,6,5,4,3,2,1]
In [18]:
[1,3..10]
[1,3,5,7,9]
In [19]:
-- [1,3..]

Remarque : en Haskell, il est possible de définir des listes en compréhension, comme en Python. Dans ce cours, nous n'utiliserons pas cette façon de faire afin de limiter la quantité de syntaxe à maîtriser.

Quelques fonctions utiles pour les listes¶

Les basiques¶

In [20]:
1 : [2, 3]
[1,2,3]
In [21]:
"Hello" : ["World"]
["Hello","World"]
In [22]:
:t (:)
(:) :: forall a. a -> [a] -> [a]

(:) prend une valeur de type a et une liste de type [a] et renvoie une nouvelle liste de type [a] constituée de la valeur suivie des éléments de la liste d'origine. La complexité de cette fonction est $O(1)$.

In [23]:
head [1,2,3,4,5]
tail [1,2,3,4,5]
1
[2,3,4,5]

Ces fonctions renvoient respectivement le premier élément et la fin d'une liste. La complexité de ces fonctions est $O(1)$.

In [24]:
[1..10] ++ [11,12]
[1,2,3,4,5,6,7,8,9,10,11,12]
In [25]:
:t (++)
(++) :: forall a. [a] -> [a] -> [a]

(++) prend deux listes de type [a] et renvoie une nouvelle liste de type [a] qui est la concaténation de ces deux listes. ⚠ La complexité de cette fonction est $O(n)$ où $n$ est la longueur de la première liste.

Exercice¶

Parmi ces expressions, lesquelles sont valides en Haskell, et pourquoi ?

In [ ]:
([2,4],[2,4,5])
In [ ]:
([2,4],['2','4','5'])
In [ ]:
[(2,4),(5,5),('a','b')]
In [ ]:
1 : (2,3)
In [ ]:
(2,4) : (2,3)
In [ ]:
(2,4) : []
In [ ]:
(2,4) ++ []
In [ ]:
head [1..10] == [1]
In [ ]:
head [1..10] + 100
In [ ]:
[1..10] * 100

Quelques fonctions supplémentaires sur les listes¶

In [35]:
length [1..2^10]
1024
In [36]:
elem 'a' ['A'..'Z']
False
In [37]:
last [1,2,3,4,5]
init [1,2,3,4,5]
5
[1,2,3,4]

Ces fonctions renvoient respectivement le dernier élément et le début d'une liste. ⚠ Attention, la complexité de ces fonctions est $O(n)$ où $n$ est la longueur de la liste.

La documentation de toutes les fonctions standard sur les listes se trouve ici.

Chaînes de caractères¶

En Haskell, les chaînes de caractères sont simplement des listes de caractères, le type String est donc équivalent au type [Char].

In [38]:
"abc" == ['a','b','c']
True
In [39]:
'a': "bcd"
"abcd"

Il existe quelques fonctions spécifiques aux caractères et aux chaînes de caractères.

In [40]:
words "Il etait une fois... \n ou deux"
["Il","etait","une","fois...","ou","deux"]
In [41]:
show 42
"42"
In [42]:
:t show
show :: forall a. Show a => a -> String
In [43]:
read "42" :: Int
42
In [44]:
:t read
read :: forall a. Read a => String -> a

Il existe également des fonctions pour manipuler les caractères dans le module Data.Char.

In [45]:
import Data.Char

isAlpha '4'
isAlpha 'A'
False
True
In [46]:
isDigit 'a'
False
In [47]:
toUpper 'a'
'A'
In [48]:
ord 'a'
97

Il en existe d'autres, consultez la documentation pour les connaître.

Pattern matching¶

Le pattern matching consiste à choisir une définition selon la forme ou la valeur des paramètres reçus par une fonction.

Une suite d’expressions syntaxiques, appelées patterns, permet de choisir entre plusieurs résultats du même type. Le pattern générique _ correspond à n’importe quelle valeur.

  • Si le premier pattern correspond, le premier résultat est choisi.
  • Sinon, si le deuxième pattern correspond, le deuxième résultat est choisi.
  • Et ainsi de suite...

On commence avec quelques exemples simples :¶

In [49]:
intToText :: Int -> String
intToText 0 = "zero"
intToText 1 = "un"
intToText 2 = "deux"
intToText _ = "non compris entre 0 et 2"
In [50]:
intToText 1
"un"
In [51]:
intToText 3
"non compris entre 0 et 2"

La factorielle, grand classique :

In [52]:
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
In [53]:
factorial 5
120

Calcul de $x^n$ :

In [54]:
pow :: Int -> Int -> Int
pow x 0 = 1
pow x 1 = x
pow x n = x * pow x (n - 1)
In [55]:
pow 2 10
1024

Le "ET" logique :

In [56]:
and' :: Bool -> Bool -> Bool
and' True  True  = True
and' True  False = False
and' False True  = False
and' False False = False
In [57]:
and' (10 == 2 *5) ('a' == head ['a'])
True
In [58]:
and' :: Bool -> Bool -> Bool
and' True  b  = b
and' False _  = False
In [59]:
and' :: Bool -> Bool -> Bool
and' True True = True
and' _    _    = False

Extraire les données d'un triplet (déconstruction):

In [60]:
first3 :: (a, b, c) -> a
first3 (x, _, _) = x

second3 :: (a, b, c) -> b
second3 (_, y, _) = y

third3 :: (a, b, c) -> c
third3 (_, _, z) = z
In [61]:
first3 (10,20,30)
second3 (10,20,30)
third3 (10,20,30)
10
20
30

Distance entre deux points $(x_1,y_1)$ et $(x_2,y_2)$ :

In [62]:
distance :: (Double, Double) -> (Double, Double) -> Double
distance (x1, y1) (x2, y2) = sqrt ((x2 - x1) ^ 2 + (y2 - y1) ^ 2)
In [63]:
distance (3,2) (1,8)
6.324555320336759

On peut utliser where combiné avec le pattern matching :

In [64]:
distance :: (Double, Double) -> (Double, Double) -> Double
distance (x, y) (0, 0) = sqrt (x ^ 2 + y ^ 2)
distance (0, 0) (x, y) = sqrt (x ^ 2 + y ^ 2)
distance p1      p2    = sqrt ((x2 - x1) ^ 2 + (y2 - y1) ^ 2)
    where
        (x1, y1) = p1
        (x2, y2) = p2
In [65]:
distance (0,0) (1,8)
8.06225774829855

Attention au pattern matching non exhaustif :

In [66]:
intToText :: Int -> String
intToText 0 = "zero"
intToText 1 = "un"
intToText 2 = "deux"

intToText 3
<interactive>:(2,1)-(4,20): Non-exhaustive patterns in function intToText

Pattern matching pour les listes¶

Le pattern matching est très utile pour définir des fonctions (récursives) sur les listes

In [67]:
-- mot d'exactement trois caractères commençant par un 'a'
test :: [Char] -> Bool
test ['a', _, _] = True
test _           = False
In [68]:
test "abc"
test "abcd"
test "cba"
True
False
False
In [69]:
isEmpty :: [a] -> Bool
isEmpty [] = True
isEmpty _  = False
In [70]:
isEmpty []
isEmpty [1..]
True
False
In [71]:
atLeastTwo :: [a] -> Bool
atLeastTwo []  = False
atLeastTwo [_] = False
atLeastTwo _   = True
In [72]:
atLeastTwo [1]
atLeastTwo [1..10]
False
True

Une liste est une structure de données récursive : elles est soit vide, soit composée d'un élément suivi une liste. Le pattern matching permet de suivre cette décomposition naturellement.

In [73]:
sum' :: [Int] -> Int
sum' []     = 0
sum' (x:xs) = x + sum' xs
In [74]:
size :: [a] -> Int
size []     = 0
size (_:xs) = 1 + size xs
In [75]:
atLeastTwo :: [a] -> Bool
atLeastTwo (x:y:xs) = True
atLeastTwo _        = False

Exemples classiques¶

On peut recoder facilement les fonctions standard sur les listes.

In [76]:
-- on suppose que la liste ne peut pas être vide, on verra plus tard comment gérer ça.
last' :: [a] -> a
last' [x]      = x
last' (_ : xs) = last' xs
In [77]:
last' [1..100]
100
In [78]:
elem' :: Eq a => a -> [a] -> Bool
elem' _ []       = False
elem' e (x : xs) = x == e || elem' e xs
In [79]:
elem' 5 [1,2,3,4,5,6,7]
elem' 15 [1,2,3,4,5,6,7]
True
False
In [80]:
reverse ["avion","ballon","chat"]
["chat","ballon","avion"]
In [81]:
reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x:xs) = (reverse' xs) ++ [x]
In [82]:
reverse' [1..10]
[10,9,8,7,6,5,4,3,2,1]

Hum... et la complexité ?

En TP¶

Recoder des fonctions classiques sur les listes en utlisant le pattern matching (voici quelques exemples).

In [83]:
take 10 [1..]
[1,2,3,4,5,6,7,8,9,10]
In [84]:
:t take
take :: forall a. Int -> [a] -> [a]
In [85]:
drop 10 ['a'..'z']
"klmnopqrstuvwxyz"

Quelle est la complexité de ces différentes fonctions ?

In [86]:
zip [1,2,3,4,5] ['a','b','c','d','e']
zip [1,2,3] ['a','b','c','d','e']
zip [1,2,3,4,5] ['a','b','c']
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]
[(1,'a'),(2,'b'),(3,'c')]
[(1,'a'),(2,'b'),(3,'c')]
In [87]:
unzip [(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]
([1,2,3,4,5],"abcde")
In [88]:
import Data.List

intersperse 0 [1..10]
intersperse ',' ['a'..'z']
[1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9,0,10]
"a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z"
In [89]:
intercalate [0] [[1,2],[3,4],[5,6]]
intercalate "-" ["b","cd","efg"]
[1,2,0,3,4,0,5,6]
"b-cd-efg"
In [ ]:

In [ ]: