I. Présentation des procédures internes▲
Avant de présenter les procédures internes, voici un petit rappel des différents types de procédures dans Windev :
- les procédures globalesDocumentation Windev sur les procédures globales : elles sont stockées dans une collection de procédures (fichiers *.wdg) et leur portée englobe tout le projet ;
- les procédures localesDocumentation Windev sur les procédures locales : elles sont stockées dans les fenêtres (fichier *.wdw) et leur portée se limite à la fenêtre. Il est tout de même possible de contourner cette règle, mais cela peut engendrer un code sale et difficile à maintenir ;
- les méthodes : elles sont stockées dans les classes (fichiers *.wdc). Elles sont utilisées dans le cadre de la POO (Programmation Orientée Objet). Leur portée peut varier en fonction de leur définition ;
- les procédures compilées dynamiquement : ce sont des procédures créées par programmation au moyen de la fonction CompileDocumentation Windev de la fonction Compile ;
- les procédures appelées à la suite d'un événement sur un champ de l'application.
Et enfin, apparues à partir de la version 20 de Windev, les procédures internesDocumentation Windev sur les procédures internes. Ce sont des procédures que l'on définit dans toutes les autres procédures citées précédemment, à l'exception des procédures compilées dynamiquement. Elles peuvent aussi être définies dans une procédure interne.
Leur portée se limite à la procédure dans laquelle elles sont définies.
I-A. Comment déclarer et utiliser une procédure interne▲
Une procédure interne est un bloc de code qui commence par les mots clés PROCEDURE INTERNE et qui se termine par le mot clé FIN.
Un exemple sera plus parlant qu'une longue explication :
PROCEDURE
MaProcédureLocaleOuGlobale()
PROCEDURE
INTERNE MaProcédureInterne()
RENVOYER
"Hello world"
FIN
Trace
(
MaProcédureInterne())
I-B. Les pièges à éviter▲
I-B-1. Appeler une autre procédure interne▲
Jusqu'à la version 22, les procédures internes ne permettent pas d'appeler une autre procédure interne définie au même niveau. Cela signifie aussi qu'une procédure interne ne peut pas s'appeler récursivement.
Par contre, une procédure interne peut appeler une autre procédure interne qui a été définie dans un niveau inférieur.
Par exemple :
PROCEDURE
MaProcédureLocaleOuGlobale()
PROCEDURE
INTERNE MaProcédureInterneNiveau1()
MaProcédureNiveau2()
// Le compilateur laisse passer
UneAutreProcédureNiveau1()
// Le compilateur indique une erreur
PROCEDURE
INTERNE MaProcédureNiveau2()
…
FIN
FIN
PROCEDURE
INTERNE UneAutreProcédureNiveau1()
…
FIN
On peut résoudre ce problème en utilisant une variable de type procédure. En l'affectant avec la procédure interne que l'on souhaite appeler, elle devient accessible par les autres.
L'exemple précédent :
PROCEDURE
MaProcédureLocaleOuGlobale()
MaVariableProcédure est
une
procédure =
UneAutreProcédureNiveau1 // Attention, pas de parenthèse
PROCEDURE
INTERNE MaProcédureInterneNiveau1()
MaProcédureNiveau2()
// Le compilateur laisse passer
MaVariableProcédure()
// Le compilateur laisse passer
PROCEDURE
INTERNE MaProcédureNiveau2()
…
FIN
FIN
PROCEDURE
INTERNE UneAutreProcédureNiveau1()
…
FIN
I-B-2. Utiliser les paramètres de niveau supérieur▲
Les procédures internes permettent d'utiliser les variables définies au niveau de la procédure parente. Cela veut dire que l'on peut utiliser les variables locales, mais aussi celles passées en paramètre.
Mais attention, dans le cas d'un paramètre, il faut obligatoirement utiliser le mot clé LOCAL. En effet, une procédure interne peut exister en dehors de sa procédure mère (nous verrons ça avec les closuresLes closures ). Dans ce cas, si le paramètre est passé en référence, alors le lien avec ce paramètre peut-être brisé dès qu'on sort du champ d'exécution de la procédure mère. C'est pourquoi le compilateur oblige l'utilisation du mot clé LOCAL.
Cela s'applique aussi si la procédure mère est une procédure interne.
Par exemple :
PROCEDURE
MaProcédureLocaleOuGlobale(
LOCAL
MonParamètre1,
MonParamètre2)
PROCEDURE
INTERNE MaProcédureInterneNiveau1()
Trace
(
MonParamètre1)
// Le compilateur laisse passer
Trace
(
MonParamètre2)
// Le compilateur génère une erreur
PROCEDURE
INTERNE MaProcédureNiveau2()
…
FIN
FIN
I-C. L'utilité des procédures internes▲
I-C-1. Factorisation du code▲
L'une des principales utilités est la factorisation du code. Avant les procédures internes, le fait de factoriser une procédure existante obligeait à la création d'une multitude de procédures de même portée. On se retrouvait donc soit avec plusieurs fonctions (locales ou globales) qui n'étaient appelées qu'une seule fois, soit on se retrouvait avec une procédure très longue pour éviter de polluer la fenêtre ou la collection de procédures.
Les procédures internes permettent donc d'éviter de polluer la fenêtre ou la collection de procédures et de produire un code de meilleure qualité qu'auparavant en respectant la notion du DRY (Don't Repeat Yourself ).
Bien entendu, si l'on se retrouve avec la même procédure interne dans plusieurs blocs de code, il faudra songer à les remplacer par des procédures globales.
I-C-2. Clarté et autodocumentation du code▲
Les procédures internes permettent aussi de clarifier la lecture du code.
En englobant dans une procédure interne un bloc de code de la procédure et en choisissant un nom compréhensible, cohérent et parlant, on peut facilement produire un code autodocumenté.
On peut ainsi se débarrasser des commentaires et conserver tout de même un code lisible et compréhensible.
I-C-3. Autres utilités▲
On abordera dans la suite de l'article que les procédures internes permettent aussi de créer des closures.
II. Les closures ▲
II-A. Présentation du concept de closure et son utilisation▲
Une closure (ou clôture/fermeture en français) est une procédure retournée par une autre procédure qui conserve le contexte dans lequel elle a été créée. Cela signifie qu'elle a toujours accès aux variables qui ont été définies dans la procédure parente même si cette dernière n'est plus en cours d'exécution.
Dans Windev, une closure est une procédure interne retournée par une procédure (quelle que soit sa nature). Cette procédure interne conserve le contexte dans lequel elle a été créée.
Un exemple permettra de mieux comprendre cette notion :
PROCEDURE
GenerateurDeCompteur(
Départ est
un
entier
)
Compteur est
un
entier
=
Départ
PROCEDURE
INTERNE Compteur()
Compteur ++
Renvoyer
Compteur
FIN
MonCompteur est
une
Procédure =
Compteur // On crée la closure ici
RENVOYER
MonCompteur
// Utilisation des compteurs
PremierCompteur est
une
procédure =
GenerateurDeCompteur(
5
)
SecondCompteur est
une
procédure =
GenerateurDeCompteur(
10
)
Trace
(
PremierCompteur())
// Affiche 6
Trace
(
PremierCompteur())
// Affiche 7
Trace
(
PremierCompteur())
// Affiche 8
Trace
(
SecondCompteur())
// Affiche 11
Trace
(
SecondCompteur())
// Affiche 12
Trace
(
PremierCompteur())
// Affiche 9
Trace
(
SecondCompteur())
// Affiche 13
Cet exemple nous montre aussi qu'il faut obligatoirement passer par des variables de type Procédure pour manipuler des closures.
Les closures présentent les avantages suivants :
- elles conservent leur état et peuvent le restituer à l'appel suivant ;
- on peut générer plusieurs fonctions identiques, mais avec un état différent.
II-B. Un exemple de closure▲
Dans Windev, lorsqu'on souhaite découper une chaîne de caractères, on peut utiliser la fonction ExtraitChaîneFonction Windev ExtraitChaîne.
MaChaine est
une
chaîne
=
"Ceci est une chaîne à parcourir"
i est
un
entier
=
1
MonMot est
une
chaîne
=
ExtraitChaîne(
MaChaine,
i,
" "
)
TANTQUE
MonMot <>
EOT
Trace
(
MonMot)
i ++
MonMot =
ExtraitChaîne(
MaChaine,
i,
" "
)
FIN
La fonction ExtraitChaîne est compliquée à utiliser, car elle utilise des paramètres qu'il faut indiquer à chaque fois. De plus, il y a un compteur qu'il ne faut pas oublier d'incrémenter.
On peut utiliser une closure pour simplifier ce code :
MaChaine est
une
chaîne
=
"Ceci est une chaîne à parcourir"
Extrait est
une
Procédure =
ExtraitChaineClosure(
MaChaine,
" "
)
MonMot est
une
chaîne
=
Extrait()
TANTQUE
MonMot <>
EOT
Trace
(
MonMot)
MonMot =
Extrait()
FIN
On constate deux améliorations sur le code précédent :
- le compteur i a disparu ;
- on n'a indiqué les paramètres qu'une seule fois, lors de la création de la closure.
Le code permettant cela est le suivant :
PROCEDURE
ExtraitChaineClosure(
LOCAL
pChaineInitiale est
chaîne
,
LOCAL
pSéparateur est
chaîne
=
TAB
,
LOCAL
pSensParcours est
un
entier
=
DepuisDébut
)
Compteur est
un
entier
=
1
PROCEDURE
INTERNE iExtrait(
pPosition est
entier
=
Compteur)
Résultat est
une
chaîne
Résultat =
ExtraitChaîne(
pChaineInitiale,
pPosition,
pSéparateur,
pSensParcours)
SI
Résultat <>
EOT
ALORS
Compteur =
pPosition +
1
FIN
RENVOYER
Résultat
FIN
Extrait est
une
Procédure =
iExtrait
RENVOYER
Extrait
III. Les closures et les structures▲
Renvoyer une variable de type procédure n'est pas la seule méthode qui permet de créer une closure. Mais on peut aussi renvoyer une variable de type structure qui contient au moins un membre de type procédure.
Cette méthode nous permet plusieurs choses :
- elle nous donne la possibilité de renvoyer plusieurs closures en simultané ;
- elle nous donne accès à une partie du contexte de la closure au travers de ses membres.
Afin de mieux comprendre ce concept, il est possible de faire une analogie avec la POO et la notion de classe :
- la structure serait la classe ;
- la fonction qui renvoie la structure serait le constructeur ;
- les membres de la structure seraient les membres publics de la classe ;
- les variables utilisées dans la fonction seraient les membres privés de la classe ;
- les membres de type procédure seraient les méthodes de la classe.
III-A. Mise en place ▲
Pour utiliser ce principe, il faut donc définir une procédure qui va renvoyer une instance de notre structure. L'instance doit forcément être stockée dans une variable de type dynamique. En effet, si l'on utilise une variable non dynamique, les membres de la structure renvoyés par la fonction seront copiés. Dans ce cas, les closures ne travailleront pas sur les membres copiés, mais sur les membres originaux.
Un exemple sera plus parlant :
Animal est
une
Structure
Manger est
une
Procédure
FaireDuBruit est
une
Procédure
Poids est
un
entier
FIN
PROCEDURE
CréerAnimal(
LOCAL
pBruit est
une
chaîne
,
pPoids est
un
entier
)
UnAnimal est
un
Animal dynamique
=
allouer
un
Animal
UnAnimal.
Poids =
pPoids
PROCEDURE
INTERNE FaireDuBruit()
Trace
(
pBruit)
FIN
UnAnimal:
FaireDuBruit =
FaireDuBruit
PROCEDURE
INTERNE Manger(
pPoidsNourriture=
1
)
UnAnimal.
poids +=
pPoidsNourriture
FIN
UnAnimal:
Manger =
Manger
RENVOYER
UnAnimal
UnChien est
un
Animal dynamique
=
CréerAnimal(
"Ouaf"
,
15
)
UnChat est
un
Animal dynamique
=
CréerAnimal(
"Miaou"
,
3
)
UnChien.
Manger()
UnChien.
Manger()
UnChien.
Manger()
UnChat.
Manger()
UnChat.
Manger(
2
)
UnChat.
Manger(
3
)
UnChat.
Manger()
UnChat.
Manger()
Trace
(
"Poids du chien"
,
UnChien.
Poids)
// Affiche 18
Trace
(
"Poids du chat"
,
UnChat.
Poids)
// Affiche 11
UnChien.
FaireDuBruit()
// Affiche Ouaf
UnChat.
FaireDuBruit()
// Affiche Miaou
III-B. Héritage▲
Il est possible de faire un simulacre d'héritage de classe. On ne peut pas utiliser le polymorphisme avec cette méthode (sauf si l'on utilise des structures identiques), mais on peut surcharger le constructeur de manière à créer deux instances ayant des comportements différents.
// On reprend la structure Animal de l'exemple précédent
PROCEDURE
CréerHumain(
pPoids est
un
entier
)
UnHumain est
un
Animal =
CréerAnimal(
""
,
pPoids)
// Appel du constructeur parent
Procédure Interne Parler()
Trace
(
"Bonjour"
)
FIN
UnHumain:
FaireDuBruit =
Parler
RENVOYER
UnHumain
III-C. Structure paramétrable▲
Il est possible de créer des structures paramétrables en passant à la procédure Constructeur le ou les procédures à associer à la structure.
Les exemples qui suivent se basent sur la structure suivante :
stAnimal est
une
Structure
Bruit est
une
chaîne
FaireDuBruit est
une
Procédure
FIN
III-C-1. Avec du code compilé dynamiquement et la fonction Compile▲
Le constructeur :
PROCEDURE
CréerAnimal(
pCodeFaireDuBruit)
UnAnimal est
un
stAnimal dynamique
=
allouer
un
stAnimal
UnAnimal:
FaireDuBruit =
Compile
(
pCodeFaireDuBruit)
RENVOYER
UnAnimal
L'appel du constructeur :
UnChien est
un
stAnimal dynamique
=
CréerAnimal(
"Trace(""Ouaf"")"
)
UnChat est
un
stAnimal dynamique
=
CréerAnimal(
"Trace(""Miaou"")"
)
UnChien.
FaireDuBruit()
// La trace affiche Ouaf
UnChat.
FaireDuBruit()
// La trace affiche Miaou
III-C-2. Avec du code exécuté dynamiquement et la fonction ExécuteCode▲
L'avantage de cette méthode est de pouvoir utiliser l'objet animal défini dans la procédure CréerAnimal.
Le constructeur :
PROCEDURE
CréerAnimal(
LOCAL
pBruit,
LOCAL
pCodeFaireDuBruit)
UnAnimal est
un
stAnimal dynamique
=
allouer
un
stAnimal
UnAnimal.
Bruit =
pBruit
PROCEDURE
INTERNE iFaireDuBruit()
ExécuteCode(
pCodeFaireDuBruit)
FIN
UnAnimal.
FaireDuBruit =
iFaireDuBruit
RENVOYER
UnAnimal
L'appel du constructeur :
UnChien est
un
stAnimal dynamique
=
CréerAnimal(
"Ouaf"
,
"Trace(""Je suis un chien qui dit "" + UnAnimal.Bruit)"
)
UnChat est
un
stAnimal dynamique
=
CréerAnimal(
"Miaou"
,
"Trace(""Je suis un chat qui dit "" + UnAnimal.Bruit)"
)
UnChien.
FaireDuBruit()
// La trace affiche Je suis un chien qui dit ouaf
UnChat.
FaireDuBruit()
// La trace affiche Je suis un chat qui dit miaou
III-C-3. Directement avec une autre procédure▲
L'avantage de cette méthode est de pouvoir utiliser les variables UnChien ou UnChat dans la procédure
Le constructeur :
PROCEDURE
CréerAnimal(
LOCAL
pBruit,
LOCAL
pFaireDuBruit est
une
Procédure)
UnAnimal est
un
stAnimal dynamique
=
allouer
un
stAnimal
UnAnimal.
Bruit =
pBruit
UnAnimal.
FaireDuBruit =
pFaireDuBruit
RENVOYER
UnAnimal
L'appel du constructeur :
UnChien est
un
stAnimal dynamique
=
CréerAnimal(
"Ouaf"
,
iChienFaireDuBruit)
UnChat est
un
stAnimal dynamique
=
CréerAnimal(
"Miaou"
,
iChatFaireDuBruit)
PROCEDURE
INTERNE iChienFaireDuBruit()
Trace
(
"Le chien fait : "
+
UnChien.
Bruit)
FIN
PROCEDURE
INTERNE iChatFaireDuBruit()
Trace
(
"Le chat fait : "
+
UnChat.
Bruit)
FIN
UnChien.
FaireDuBruit()
// La trace affiche Le chien fait Ouaf
UnChat.
FaireDuBruit()
// La trace affiche Le chat fait Miaou
III-C-4. Avec une closure▲
Le constructeur est identique à l'exemple précédent. Par contre, l'appel diffère :
L'appel du constructeur :
PROCEDURE
INTERNE iClosureFaireDuBruit(
LOCAL
pAnimal ,
LOCAL
pBruit)
PROCEDURE
INTERNE iFaireDuBruit()
Trace
(
ChaîneConstruit
(
"Le %1 fait : %2"
,
pAnimal,
pBruit))
FIN
FaireDuBruit est
une
Procédure =
iFaireDuBruit
RENVOYER
FaireDuBruit
FIN
UnChien est
un
stAnimal dynamique
=
CréerAnimal(
"Ouaf"
,
iClosureFaireDuBruit(
"chien"
,
"Ouaf"
))
UnChat est
un
stAnimal dynamique
=
CréerAnimal(
"Miaou"
,
iClosureFaireDuBruit(
"chat"
,
"Miaou"
))
UnChien.
FaireDuBruit()
// La trace affiche Le chien fait : Ouaf
UnChat.
FaireDuBruit()
// La trace affiche Le chat fait : Miaou
III-D. Pour conclure sur les structures▲
Cette méthode ne remplace pas les classes Windev qui resteront toujours plus puissantes. Mais lorsqu'on a besoin d'avoir une multitude d'objets simples, mais différents, cette pratique permet d'éviter de créer une multitude de fichiers, car dans Windev, une classe correspond à un fichier. De plus, la portée de ces structures dépend de l'endroit où on les déclare. Tandis qu'une classe est globale au projet, ces structures peuvent exister seulement au sein d'une fonction ou d'une fenêtre.
IV. Conclusion et remerciements▲
Cet article a été inspiré par trois de mes billets de blogs (blog.ytreza.org). Vous y trouverez toutes sortes de publications concernant Windev.
Je tiens à remercier Guillaume BAYLE qui m'a donné des idées pour compléter cet article.
Je tiens aussi à remercier Guillaume SIGUI pour son accueil au sein de la communauté et la relecture de cet article.
Un grand merci à Claude LELOUP pour avoir relu et corrigé cet article.
Enfin, merci à vous d'avoir lu cet article.