16. Structures et fonctions▲
Dès lors que nous savons créer de nouveaux types composites, l'étape suivante consiste à écrire des fonctions qui prennent des objets définis par le programmeur comme paramètres et les retournent en tant que résultats. Dans ce chapitre, nous présentons également le « style de programmation fonctionnelle » et deux nouveaux plans de développement de programmes.
16-1. Heures, minutes et secondes▲
Comme autre exemple de type composite, nous allons définir une struct
appelée MyTime qui enregistre l'heure associée à un moment de la journée. Voici la définition de la structure :
2.
3.
4.
5.
6.
7.
8.
9.
"""
Représentation d'un moment de la journée.
Champs: heure, minute, seconde
"""
struct
MyTime
heure
minute
seconde
end
Le mot Time étant déjà utilisé dans Julia, choisissons MyTime pour éviter tout conflit et créons un nouvel objet MyTime :
2.
julia>
time =
MyTime(
11
, 59
, 30
)
MyTime(
11
, 59
, 30
)
Le diagramme d'objet pour MyTime est représenté à la figure 16.1.1.
|
16-1-1. Exercice 16-1▲
Écrivez une fonction appelée printtime qui prend un objet MyTime et l'affiche sous la forme heure:minute:seconde. La macro @printf
du module StdLib Printf affiche un entier avec le format "%02d"
en utilisant au moins deux chiffres, y compris un zéro de tête si nécessaire.
16-1-2. Exercice 16-2▲
Écrivez une fonction booléenne appelée isafter qui prend deux objets MyTime, t1 et t2, et qui retourne true
si t1 suit chronologiquement t2 et false
dans le cas contraire. Défi : n'utilisez pas de test if
.
16-2. Fonctions pures▲
Dans les prochaines sections, nous allons écrire deux fonctions qui additionnent des valeurs de temps. Le but est d'appréhender deux types de fonctions : les fonctions pures et les modificateurs. Nous verrons également un plan de développement appelé prototype et correctifs. Un plan de développement est un procédé permettant de s'attaquer à un problème complexe en commençant par un prototype simple et en y incorporant graduellement des éléments qui le complexifient.
Voici un prototype simple addtime :
2.
3.
function
addtime(
t1, t2)
MyTime(
t1.heure +
t2.heure, t1.minute +
t2.minute, t1.seconde +
t2.seconde)
end
La fonction crée un nouvel objet MyTime, initialise ses champs et retourne une référence au nouvel objet. On parle de fonction pure, car elle ne modifie aucun des objets qui lui sont transmis en tant qu'arguments. En outre, elle n'a aucun autre effet (tel l'affichage d'une valeur ou l'obtention d'une entrée utilisateur) que le renvoi d'une valeur.
Pour tester cette fonction, créons deux objets MyTime :
-
start contient l'heure de début d'un film, comme Le nom de la rose de Jean-Jacques Annaud ;
- duration contient la durée du film (2 heures 11 minutes).
addtime indique quand le film sera terminé.
2.
3.
4.
5.
6.
7.
julia>
start =
MyTime(
9
, 55
, 0
)
;
julia>
duration =
MyTime(
2
, 11
, 0
)
;
julia>
done =
addtime(
start, duration)
;
julia>
printtime(
done)
# voir exercice 16.1.1
11
:66
:00
Le résultat 11:66:00 est à peine inattendu. Le problème vient de ce que cette fonction ne traite pas les cas où le nombre de secondes ou de minutes dépasse 60. Lorsque cela se produit, il est nécessaire de « reporter » les secondes supplémentaires dans la colonne des minutes et/ou les minutes supplémentaires dans la colonne des heures. Voici une version améliorée :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
function
addtime(
t1, t2)
seconde =
t1.seconde +
t2.seconde
minute =
t1.minute +
t2.minute
heure =
t1.heure +
t2.heure
if
seconde >=
60
seconde -=
60
minute +=
1
end
if
minute >=
60
minute -=
60
heure +=
1
end
MyTime(
heure, minute, seconde)
end
Bien que cette fonction soit correcte, elle commence à s'allonger significativement. Ultérieurement (section 16.4Prototypage ou planification ?), nous considérerons une version plus courte.
16-3. Modificateurs▲
Il est parfois utile pour une fonction de modifier les objets qu'elle reçoit en paramètre. Dans ce cas, les modifications sont visibles pour l'appelant. Les fonctions qui procèdent de cette manière sont appelées des modificateurs.
La fonction increment! qui ajoute un nombre donné de secondes à un objet MyTime peut être écrite naturellement comme un modificateur. Voici une ébauche :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
function
increment!(
time, secondes)
time.seconde +=
secondes
if
time.seconde >=
60
time.seconde -=
60
time.minute +=
1
end
if
time.minute >=
60
time.minute -=
60
time.heure +=
1
end
end
La première ligne effectue l'opération de base ; le reste traite des cas spéciaux que nous avons vus auparavant.
Cette fonction est-elle correcte ? Que se passe-t-il si secondes est bien supérieur à 60 ?
Dans ce cas, il ne suffit pas d'effectuer l'opération une seule fois. Nous devons poursuivre jusqu'à ce que time.second soit inférieur à soixante. Une solution consiste à remplacer les déclarations if
par des déclarations while
. Cela rendrait la fonction correcte, mais peu efficace.
Tout ce qui peut être fait avec des modificateurs peut également l'être avec des fonctions pures. En fait, certains langages de programmation n'autorisent que des fonctions pures. Il est prouvé que les programmes qui utilisent des fonctions pures sont plus rapides à développer et moins sujets aux erreurs que ceux recourant aux modificateurs. Cependant, les modificateurs sont parfois pratiques et les programmes fonctionnels ont tendance à être moins efficaces à l'exécution.
En général, il est recommandé d'écrire des fonctions pures chaque fois que cela est raisonnable et de ne recourir aux modificateurs que s'il existe un avantage incontestable. Cette approche pourrait être appelée : style de programmation fonctionnelle.
16-3-1. Exercice 16-3▲
Écrivez une version correcte de la fonction increment! qui ne contienne aucune boucle.
16-3-2. Exercice 16-4▲
Écrivez une version pure de la fonction increment, qui crée et retourne un nouvel objet MyTime plutôt que de modifier le paramètre.
16-4. Prototypage ou planification ?▲
Le plan de développement exposé ci-dessus est dénommé « prototype et correctifs » (prototype and patches). Pour chaque fonction, nous avons écrit un prototype qui a effectué un calcul de base. Ensuite après l'avoir testé, nous en avons corrigé les erreurs graduellement.
Cette approche peut être efficace notamment lorsque le programmeur n'a pas encore une compréhension approfondie du problème. Ceci étant, les corrections incrémentales peuvent conduire à un code inutilement compliqué — puisqu'il traite de nombreux cas particuliers — et peu fiable — étant donné qu'il est difficile de connaître toutes les erreurs traitées.
Une autre option consiste en un développement planifié dans lequel une compréhension de haut niveau du problème peut rendre la programmation nettement plus aisée. Dans ce cas, l'idée est qu'un objet Time est en réalité un nombre composé de trois nombres en base 60 (voir le système sexagécimal).
Lorsque nous avons écrit addtime and increment!, nous faisions effectivement de l'addition en base 60, c'est pourquoi nous devions passer d'une colonne à l'autre.
Cette observation suggère une autre approche de l'ensemble du problème : nous pouvons convertir les objets MyTime en nombres entiers et profiter du fait que l'ordinateur est capable de faire de l'arithmétique des nombres entiers.
Voici une fonction qui convertit les objets MyTime en nombres entiers :
2.
3.
4.
function
timetoint(
time)
minutes =
time.heure *
60
+
time.minute
secondes =
minutes *
60
+
time.seconde
end
Et voici une fonction qui convertit un entier en un objet de type MyTime (rappelons que divrem divise le premier argument par le second et retourne le quotient ainsi que le reste sous forme de tuple) :
2.
3.
4.
5.
function
inttotime(
secondes)
(
minutes, seconde)
=
divrem(
secondes, 60
)
heure, minute =
divrem(
minutes, 60
)
MyTime(
heure, minute, seconde)
end
Il faudra peut-être réfléchir un peu et faire quelques tests pour se convaincre que ces fonctions sont correctes. Une façon de les tester est de vérifier que timetoint(
inttotime(
x))
==
x pour de nombreuses valeurs de x. Ceci constitue un exemple de contrôle de cohérence.
Une fois convaincu que ces fonctions sont correctes, il devient possible de les utiliser afin de réécrire addtime :
2.
3.
4.
function
addtime(
t1, t2)
secondes =
timetoint(
t1)
+
timetoint(
t2)
inttotime(
secondes)
end
Cette version est plus courte que l'originale et plus facile à vérifier.
Il est plus difficile de passer de la base 60 à la base 10 (et inversement) que de jongler avec les heures, minutes et secondes. La conversion de base est plus abstraite. La manipulation de valeurs heures/minutes/secondes est plus spontanée.
Cependant, si nous pouvions traiter ces valeurs comme des nombres en base 60(42) et faisions l'investissement d'écrire les fonctions de conversion (timetoint et inttotime), nous obtiendrions un programme plus court, plus facile à lire et à déboguer et… plus fiable.
Il est également plus aisé d'ajouter des fonctions par la suite. Par exemple, imaginons qu'il faille soustraire deux MyTime pour trouver la durée qui les sépare. Une approche naïve serait de mettre en œuvre la soustraction par emprunt. L'utilisation des fonctions de conversion serait plus facile en base 60 et aurait plus de chances d'être correcte.(43)
16-4-1. Exercice 16-5▲
Réécrivez increment! en utilisant timetoint et inttotime.
16-5. Débogage▲
Un objet MyTime est bien formé si les valeurs minute et seconde sont comprises entre 0 et 60 (y compris 0, mais pas 60) et si l'attribut heure est positif. heure et minute devraient être des valeurs entières, mais nous pourrions permettre à seconde d'être exprimée avec une partie fractionnaire.
De telles exigences sont appelées des invariants, car elles doivent toujours être vraies. En d'autres termes, si elles ne sont pas vraies, c'est que quelque chose dysfonctionne.
L'écriture d'un code pour vérifier les invariants peut aider à détecter les erreurs et à en trouver les causes. Par exemple, nous pourrions disposer d'une fonction comme isvalidtime qui prend un objet MyTime et retourne false
si ce dernier viole un invariant :
2.
3.
4.
5.
6.
7.
8.
9.
function
isvalidtime(
time)
if
time.heure <
0
||
time.minute <
0
||
time.seconde <
0
return
false
end
if
time.minute >=
60
||
time.seconde >=
60
return
false
end
true
end
Au début de chaque fonction, il est pertinent de vérifier les arguments pour s'assurer qu'ils sont valides :
2.
3.
4.
5.
6.
7.
function
addtime(
t1, t2)
if
!
isvalidtime(
t1)
||
!
isvalidtime(
t2)
error(
"objet MyTime non valide dans add_time"
)
end
seconds =
timetoint(
t1)
+
timetoint(
t2)
inttotime(
secondes)
end
Une macro @assert
peut être utilisée à la place, afin de vérifier un invariant donné et d'émettre une exception en cas d'échec :
2.
3.
4.
5.
function
addtime(
t1, t2)
@assert
(
isvalidtime(
t1)
&&
isvalidtime(
t2)
, "objet MyTime non valide dans add_time"
)
secondes =
timetoint(
t1)
+
timetoint(
t2)
inttotime(
secondes)
end
Les macros @assert
sont utiles, car elles permettent de distinguer le code qui traite des conditions normales de celui qui vérifie les erreurs.
16-6. Glossaire▲
prototype et correctifs plan de développement qui implique la rédaction d'une ébauche de programme, le test et la correction des erreurs au fur et à mesure que ces dernières sont détectées.
planification plan de développement qui implique une compréhension de haut niveau d'un problème et une planification plus poussée que le développement progressif ou le développement de prototypes/correctifs.
fonction pure fonction qui ne modifie aucun des objets qu'elle reçoit comme arguments. La plupart des fonctions pures possèdent une valeur de retour.
modificateur fonction qui modifie un ou plusieurs des objets qu'elle reçoit comme arguments. La plupart des modificateurs sont vides (ou nuls), c'est-à-dire qu'ils ne retournent rien.
style de programmation fonctionnelle style de conception de programme dans lequel la majorité des fonctions sont pures.
invariant paramètre ou attribut qui ne devrait jamais changer pendant l'exécution d'un programme.
16-7. Exercices▲
16-7-1. Exercice 16-6▲
Écrivez une fonction appelée multime qui prend un objet MyTime ainsi qu'un nombre et qui retourne un nouvel objet MyTime contenant le produit du MyTime original et du nombre.
Utilisez ensuite multime pour écrire une fonction qui prend un objet MyTime représentant le temps d'arrivée dans une course ainsi qu'un nombre figurant la distance. Cette fonction doit retourner un objet MyTime qui donne l'allure moyenne (durée par kilomètre parcouru).
16-7-2. Exercice 16-7▲
Julia fournit des objets « temps » similaires à MyTime, développé dans ce chapitre. Toutefois, ils offrent un riche ensemble de fonctions et d'opérateurs. Lisez la documentation à l'adresse Dates.
- Écrivez un programme qui recueille la date du jour et affiche le jour de la semaine.
- Écrivez un programme qui accepte une date d'anniversaire en entrée et affiche l'âge de l'utilisateur ainsi que le nombre de jours, d'heures, de minutes et de secondes jusqu'à son prochain anniversaire.
-
Pour deux personnes nées à des jours différents, il existe un jour où l'une d'elles est deux fois plus âgée que l'autre. Écrivez un programme qui prend deux dates anniversaires et calcule DoubleJour.
- Défi : écrivez une version plus générale qui calcule le jour où une personne est n fois plus âgée qu'une autre.