Les procédures internes et les closures : une nouvelle manière de coder en Windev

Jonathan LAURENT

Ce tutoriel va vous apprendre à programmer avec les procédures internes sous WinDev. C'est une fonctionnalité apparue dans la version 20 de WinDev. À chaque version, de nouvelles fonctionnalités apparaissent. Il est possible que des exemples cités ici ne fonctionnent pas en version 20.

Un espace de discussion vous est proposé sur le forum pour partager votre avis.

Commentez

- Titre : Les procédures internes et les closures : une nouvelle manière de coder en Windev

- Auteur : Jonathan LAURENT

- Parution : 13 octobre 2017

- Mise à jour : 30 octobre 2017

- Public visé :

- Licence : Le contenu de cet article est rédigé par Jonathan LAURENT et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Présentation des procédures internes

- I-A. Comment déclarer et utiliser une procédure interne

- I-B. Les pièges à éviter

- I-B-1. Appeler une autre procédure interne

- I-B-2. Utiliser les paramètres de niveau supérieur

- I-C. L'utilité des procédures internes

- I-C-1. Factorisation du code

- I-C-2. Clarté et autodocumentation du code

- I-C-3. Autres utilités

- II. Les closures

- II-A. Présentation du concept de closure et son utilisation

- II-B. Un exemple de closure

- III. Les closures et les structures

- III-A. Mise en place

- III-B. Héritage

- III-C. Structure paramétrable

- III-C-1. Avec du code compilé dynamiquement et la fonction Compile

- III-C-2. Avec du code exécuté dynamiquement et la fonction ExécuteCode

- III-C-3. Directement avec une autre procédure

- III-C-4. Avec une closure

- III-D. Pour conclure sur les structures

- IV. Conclusion et remerciements

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 globales : elles sont stockées dans une collection de procédures (fichiers *.wdg) et leur portée englobe tout le projet ;

- 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 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 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 :

Déclaration d'une procédure interne

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 :

On ne peut pas appeler des procédures internes de même niveau

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 :

On peut appeler des variables procédures

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 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 :

Utiliser le mot clé LOCAL

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 :

Exemple de closure

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îne.

Exemple de la fonction 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 :

Code utilisant la closure

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 :

Code de la closure ExtraitChaineClosure

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 :

closure et structure

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.

Exemple de surcharge de constructeur

// 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 :

Structure Animal

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 :

Constructeur code compilé dynamiquement

PROCEDURE CréerAnimal(pCodeFaireDuBruit)
UnAnimal est un stAnimal dynamique = allouer un stAnimal
UnAnimal:FaireDuBruit = Compile(pCodeFaireDuBruit)
RENVOYER UnAnimal

L'appel du constructeur :

Appel du constructeur avec du code compilé dynamiquement

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 :

Constructeur avec fonction ExécuteCode

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 :

Appel du constructeur avec ExécuteCode

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 :

Constructeur avec une procédure

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 :

Appel du constructeur avec procédure

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 :

Appel avec une closure

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.