Précédent Index Suivant

Spécification d'un problème

 
 
Concepts et terminologie  
Lorsqu'un donneur d'ordre (un client, un chef, un enseignant...) vous demande d'écrire un logiciel, il vous fournit un cahier des charges.
 
Par exemple, un client vous demande de pouvoir calculer l'aire d'un disque.
 
 
Spécification d'un problème en informatique  
La spécification consiste à décrire, le plus précisément possible, la fonctionnalité du futur logiciel (ou d'un composant d'un futur logiciel): on doit dire ce que le logiciel doit faire (le quoi).
 
Après cette phase indispensable de spécification, on devra écrire le programme (on dira que l'on implante le logiciel). Pour cela, on se demandera comment on peut faire faire à l'ordinateur la tâche attendue, celle qui est définie par la spécification.
 
On spécifie un problème -- tout au moins pour le type de problèmes que nous traiterons dans ce cours -- en répondant le plus précisément possible aux trois questions suivantes:  
 
 

 
Exemple:  
 

 
Interface, sémantique et implantation  
En informatique, on a l'habitude de scinder la spécification en deux parties: l'interface et la sémantique. Prenons une métaphore pour expliquer ces deux notions: définissons un mot de la langue française qui nous était jusqu'alors inconnu, par exemple le mot « lapin ». Dans un premier temps, on peut dire que c'est un nom commun masculin. Nous pouvons alors écrire des phrases comme « le chasseur tue le lapin » ou « ce matin, un lapin a tué un chasseur ». Ce sont deux phrases qui sont parfaitement correctes en français, elles sont syntaxiquement correctes. Mais la seconde phrase choque celui qui connaît le sens du mot « lapin »: la définition du mot doit aussi indiquer quel est son sens (« Petit mammifère (lagomorphes) à longues oreilles... »).
 
De même, la spécification d'un composant logiciel doit être vue sous deux points de vue:  
Naturellement, pour que le programme s'exécute, on doit dire aussi comment le composant logiciel doit être calculé. Un composant logiciel est donc un triptyque (les deux premiers points constituant la spécification):  
La solution informatique d'un problème est un programme constitué, en ce qui nous concerne, de fonctions. L'interface correspond alors à la signature de la fonction.
 
Exemple trivial:
 
interface (signature):
nous nommerons aire-disque cette fonction qui a un paramètre nombre et qui rend un nombre,
sémantique:
cette fonction rend la surface du disque de rayon le nombre donné;
implantation:
une implantation triviale:
 
  (define (aire-disque r) 
    (* 3.1416 r r)))
 

 
Noter que pour ce problème trivial, il y a peu d'autres implantations. Il n'en est pas de même si l'on veut calculer l'aire d'une surface plus compliquée. En règle générale, pour un problème donné, il y a de nombreuses implantations possibles, ces implantations étant plus ou moins efficaces. Noter aussi que l'on peut très bien utiliser un composant logiciel -- à partir du moment où l'on connait son interface et sa sémantique -- sans connaître son implantation. Il en va ainsi de toutes les primitives du langage.
 
Pratiquement  
Écriture de la spécification (convention)  
Rappelons que dans nos programmes Scheme, nous écrivons la spécification des fonctions sous forme de commentaires -- avec trois points-virgules -- placés avant la définition. Exemple:  
;;; aire-disque: Nombre -> Nombre 
;;; (aire-disque r) rend l'aire d'un disque de rayon «r» 
  (* 3.1416 r r))
 

 
Remarquer:  
 
Utilisation de la spécification  
Lorsqu'on utilise une fonction (i.e. lorsqu'on écrit une application de cette fonction), il faut  
  1. utiliser la signature (interface) donnée par la spécification de la fonction,
  2. utiliser la sémantique donnée par la spécification de la fonction.
 
 

 
En ce qui concerne l'utilisation de la sémantique, il faut tout simplement considérer que la fonction rend -- exactement, sans se poser d'autres questions -- ce qui est dit.
 
Nous avons déjà dit que l'utilisation, par le programmeur de sa connaissance de la signature permettait d'éviter des erreurs de compilation (pas de faute d'orthographe dans le nom de la fonction, application ayant un bon nombre d'arguments). Mais il faut aussi utiliser la connaissance, présente dans la signature, des types des données et du résultat en effectuant une vérification de type.
 
Vérification de type  
Rappelons qu'un programme Scheme est une suite de définitions de fonctions et d'expressions, l'interprète affichant les résultats de l'évaluation des expressions dans l'environnement qui contient les définitions.
 
Notons que dans un programme réel, il y a beaucoup de définitions de fonctions et une seule expression: nous n'effectuerons la vérification de type que pour les définitions de fonctions.
 
Définition de fonction
 
Pour que la définition  
;;; f : a * b -> g
(define (f a b) exp)  

 
soit correcte vis-à-vis du typage, il faut que  
Ainsi, pour vérifier qu'une définition de fonction est correcte vis-à-vis des types, la question est de savoir si une expression (au départ l'expression de la définition)  
Expressions présentes dans une définition de fonction
 
Dans cette étude nous ne traiterons que deux sortes d'expressions, les applications et les alternatives, et la vérification consiste donc à vérifier qu'une expression est de type d:
  1. lorsque l'expression est une application, elle est de la forme (g e1 e2) et pour qu'elle soit de type d:
    • il faut que le nombre d'arguments de l'application soit égal au nombre de types des donnés de g,
    • il faut que le type du résultat de g (type qui est donné par la signature de g) soit d,
    • si le type des données de g est a1 * b1 (type qui est donné par la signature de g), il faut que l'expression e1 (resp. e2) soit de type a1 (resp. b1).

     
  2. lorsque l'expression est une alternative, elle est de la forme (if c e1 e2) et pour qu'elle soit de type d, il faut que
    • c soit de type bool (prédicat),
    • les expressions e1 et e2 soient de type d.
     
 
Exemple: considérons la définition suivante:  
;;; max: Nombre * Nombre -> Nombre 
(define (max m n)
  (if (< m n) n m))
 

 
pour vérifier qu'elle est bien typée, il faut:
  1. vérifier l'en-tête de la définition de la fonction...
  2. vérifier le type de l'expression (qui doit être Nombre lorsque m et n sont de type Nombre):
     
    c'est une alternative...
    1. la condition est une application (qui doit être de type bool ou h + #f)...
    2. la conséquence est la variable n qui est bien, par hypothèse, de type Nombre,
    3. l'alternant est la variable m qui est bien, par hypothèse, de type Nombre.
     

 
Recommandation très importante  
Toutes les fois que vous écrivez une définition de fonction, vous devez vérifier son typage, cela évitera un très grand nombre d'erreurs.
Pour en savoir plus:

 
À propos des erreurs  
Taxinomie des erreurs  
On peut déjà classer les erreurs en deux catégories:
 
 
 

 
Clairement, les erreurs qui posent le plus de problèmes sont les erreurs qui ne sont pas détectées: le programme affiche (ou imprime) un résultat, qui a l'air d'un résultat, et l'utilisateur s'en sert comme tel, mais qui n'est pas le bon résultat. Le plus souvent ce sont des erreurs de conception du programme (le remède étant de programmer avec méthode, en particulier de toujours vérifier le typage des définitions de fonctions), mais cela peut être dû aussi à une mauvaise utilisation d'un logiciel mal écrit (nous reviendrons sur ce point dans un instant).
 
Classons maintenant les erreurs détectées par l'évaluateur:
 
Erreurs détectées par l'évaluateur  
 

 
Erreurs et spécification  
Lorsque nous écrivons «  ;;; aire-disque: Nombre -> Nombre », nous indiquons à l'utilisateur de cette fonction qu'il faut que la donnée de cette fonction soit un nombre. Lorsqu'il écrit une application de cette fonction, un utilisateur doit alors obligatoirement vérifier que les arguments sont bien des valeurs numériques, sous peine d'avoir une erreur détectée lors de l'évaluation, voire, pire, un résultat sans signification.
 
On peut aussi préciser le domaine de validité, ce que nous faisons en écrivant la contrainte derrière le type et entre /. Par exemple, le rayon d'un disque doit être positif ou nul:
 
;;; aire-disque: Nombre/>=0/ -> Nombre 
 

 
Considérons maintenant le problème du calcul de l'aire d'une couronne. La donnée est constituée par deux nombres positifs, le résultat est un nombre (positif) égal à l'aire de la couronne de rayon extérieur le premier nombre donné et de rayon intérieur le second nombre donné. Mais la signature:  
;;; aire-couronne: Nombre/>=0/ * Nombre/>=0/ -> Nombre 
 

 
n'est pas complètement satisfaisante. En effet, pour que les données aient un sens, il faut de plus que le rayon extérieur soit supérieur ou égal au rayon intérieur. Que fait-on dans le cas contraire? Deux solutions:  
Voyons tout d'abord la première implantation (la seconde est triviale):  
(define (aire-couronne r1 r2)
  (if (< r1 r2)
      (erreur 'aire-couronne 
             "rayon extérieur (" r1 ") <" 
             "rayon intérieur (" r2 ")")
      (- (aire-disque r1) (aire-disque r2))))

 

 
et un exemple d'application:
 
(aire-couronne 5 7)

aire-couronne: ERREUR: rayon extérieur ( 5 ) < rayon intérieur ( 7 )  

 
Noter la fonction (erreur) utilisée pour signifier l'erreur:  
Nous disposons aussi d'une fonction (erreur?) qui teste si une fonction, appliquée à des arguments donnés, provoque une erreur. Par exemple:  
(erreur? aire-couronne 5 3) -> #F
 
(erreur? aire-couronne 5 7) -> #T  

 
Pour signifier à l'utilisateur que cette fonction signifie une erreur lorsque r1 est inférieur à r2, nous l'indiquons après la signature:  
;;; aire-couronne : Nombre/>=0/ * Nombre/>=0/ -> Nombre 
;;; (aire-couronne r1 r2) rend l'aire de la couronne de rayon extérieur «r1» et de rayon intérieur «r2»  
;;; ERREUR lorsque r1 < r2 
(define (aire-couronne r1 r2)
  (if (< r1 r2)
      (erreur 'aire-couronne 
             "rayon extérieur (" r1 ") <" 
             "rayon intérieur (" r2 ")")
      (- (aire-disque r1) (aire-disque r2))))
 

 
Noter que la détection de l'erreur a un certain coût en temps alors qu'il se peut que, lors de l'utilisation de la fonction, nous soyons sûrs qu'elle est utilisée dans de bonnes conditions (parce que les arguments sont calculés par programme, programme tel que...). Il est alors dommage de perdre du temps d'ordinateur pour rien. Dans ce cas, l'implantation ne fait pas la vérification. Il est alors indispensable --- car on est dans le cas où le programme rend un résultat, celui-ci n'ayant pas de sens --- de l'indiquer aux utilisateurs de cette fonction:  
;;; aire-couronne-sans : Nombre/>=0/ * Nombre/>=0/ -> Nombre 
;;; (aire-couronne-sans r1 r2) rend l'aire de la couronne de rayon extérieur  
;;; «r1» et de rayon intérieur «r2» 
;;; HYPOTHÈSE : r1 >= r2 
(define (aire-couronne-sans r1 r2)
  (- (aire-disque r1) (aire-disque r2)))
 

 
En résumé, lorsqu'on utilise une fonction, on doit suivre la spécification, sachant que  
 
 

 



Auteur(s): titou@ufr-info-p6.jussieu.fr.Mainteneur de la page: titou@ufr-info-p6.jussieu.fr.
 

Précédent Index Suivant