# Initiation au Lua avec Scribunto/Méta-tables

Dans ce chapitre, nous allons étudier les méta-tables. La tradition voudrait que l’on explique les méta-tables de façon hermétique pour que personne n'y comprenne rien (voir, par exemple, mw:Extension:Scribunto/Lua reference manual/fr, paragraphe sur les méta-tables). Nous allons essayer toutefois, dans ce chapitre, de déroger à la tradition, quitte à s'attirer les foudres de ceux qui voudraient que cette partie reste réservée à de rares initiés.

## Position du problème

Si l’on se pose la question : "Quels sont les objets qui sont le plus susceptibles de provoquer une erreur de script ?", la réponse serait, tout naturellement "les tables". En effet, si l’on nous demande de provoquer une erreur de script, en utilisant des tables, nous n'avons que l'embarras du choix. On peut y arriver en utilisant une clé inexistante dans la table et concaténer le retour (nil) avec une chaîne de caractères. On peut essayer d'additionner deux tables t1 et t2 (t1 + t2). On peut essayer de les multiplier, les soustraire, les diviser, les concaténer, etc. Bref, aucune opération n'est possible avec les tables ! Pourtant, si l’on programme dans un domaine particulier comme, par exemple, les mathématiques, on aimerait bien pouvoir effectuer certaines opérations directement sur les tables sans provoquer d'erreur de script.
Prenons un exemple : En mathématique, on peut représenter les coordonnées d'un point, dans un repère, par une matrice colonne et si l’on souhaite faire un programme réalisant certains calculs sur les points dans l'espace, on va naturellement ranger les coordonnées d'un point dans une table avec clé numérique ou pas.
Si le point B a pour coordonnées (3,7) et le point C a pour coordonnée (1,8), on écrira
avec clé numérique:
avec clé sous forme de chaîne de caractères :
Si, dans notre programme, on souhaite calculer le milieu I du segment [BC], on aimerait bien écrire :
$I={\frac {B+C}{2}}$
Mais ce n’est pas possible car B + C provoque une erreur de script.
Les concepteurs du Lua ont donc eu l’idée de rendre possible les opérations sur les tables qui provoquait, par exemple, une erreur de script. Mais comme une même opération peut ne pas opérer de la même façon sur les tables selon le domaine dans lequel on programme ou selon ce que contient la table, il va falloir indiquer, pour chaque table, comment cette opération devra agir. La méthode opératoire que l’on va programmer et qui permettra à une opération (qui avant provoquait une erreur de script ou un effet particulier) de devenir valide sera appelée méta-méthode. La méta-méthode de la multiplication peut, par exemple, être différente selon que l’on multiplie des tables contenant des coordonnées (produit scalaire de deux vecteurs) ou que l’on multiplie des tables contenant des matrices (produit matriciel). Les méta-méthodes seront donc mémorisées dans des tables que l’on associera aux tables utilisant ces méta-méthodes. Une table ainsi associée à une autre table sera appelée méta-table.
Nous retiendrons qu'une méta-table, c’est déjà une table (comme les autres tables) mais que l’on va associer à d'autres tables grâce à une fonction appelée setmetatable. Par exemple si l’on appelle a et b deux tables et si l’on veut que b soit la méta-table de a, on écrira :
Si la table b contient une méta-méthode décrivant ce que doit donner l'opérateur + si on l'applique à deux tables, alors on pourra se permettre d'écrire :
sans que cela ne provoque une erreur de script.
On remarque, dans l'exemple ci-dessus, que l’on a défini b comme étant la méta-table de a mais pas celle de d. Ce n’est pas grave, il suffit que la méta-méthode soit définie dans la méta-table de l'une des deux tables a ou d pour que cela marche.
Plus précisément, pour les opérateurs addition, soustraction, multiplication, division, puissance, modulo et concaténation, le Lua commence par chercher la méta-table de la table avant l'opérateur et regarde si celle-ci possède une méta-méthode concernant l'opérateur. Dans le cas contraire, le Lua cherche la méta-table de la table écrite après l'opérateur ainsi qu'une méta-méthode appropriée. Nous voyons, par conséquent, qu'une méta-méthode dans l'opérateur de gauche est prioritaire.
La façon de procéder sera, par contre, différente pour les opérateurs relationnels comme inférieur, supérieur et égal ou chaque table concernée devra avoir une méta-table avec la même méta-méthode.
Si l’on souhaite obtenir la méta-table d'une table, nous utiliserons la fonction getmetatable qui retourne la méta-table d'une table passée en paramètre (sauf s'il existe une méta-méthode disant de retourner autre chose).

## Comment programmer une méta-méthode

Une méta-méthode, c’est tout simplement une fonction. Nous savons que les tables peuvent contenir des fonctions. Programmer une méta-méthode revient donc à créer une fonction que l’on va mémoriser dans une méta-table. Il y a juste une petite règle à respecter. Il faut que la chaîne de caractère qui sert de clé pour indexer la fonction décrivant une méta-méthode commence par deux soulignées comme, par exemple, __index. Prenons un premier exemple simple pour comprendre comment procéder :
Nous savons que si l’on essaye d'accéder, dans une table, à une clé qui n'existe pas, nous obtenons nil. Nous allons essayer de changer ceci par une méta-méthode qui fera en sorte, qu'au lieu d'obtenir nil, on obtienne le message "(La clé spécifiée n'existe pas dans cette table)"
Nous écrirons le programme suivant qui se trouve dans le Module:Méta
En tapant {{#invoke:Méta|mtable}}, on obtient : a la clé 1, on trouve : Piano a la clé 2, on trouve : Boule a la clé 3, on trouve : Tortue a la clé 4, on trouve : Nénuphar a la clé 5, on trouve : (La clé spécifiée n'existe pas dans cette table)
Sans méta-méthode se trouvant dans la méta-table mt associé à t, le programme précédent aurait provoqué une erreur de script car on essaye de concaténer t[5] qui n'existe pas (et qui vaut donc nil) à une chaîne de caractère.
On pourrait le vérifier en supprimant simplement l'instruction setmetatable(t,mt)
Si nous analysons le programme précédent, nous voyons qu’il n'y a rien de bien sensationnel si ce n'est la fonction :
qui demande quelques explications.
Tout d’abord, comme nous l'avons vu dans les chapitres précédents, mt.__index pourrait s'écrire mt["__index"] pour mettre en évidence le fait que __index est une clé sous forme de chaîne de caractères permettant l'accès ici à une fonction se trouvant dans la table mt. Comme nous avons affaire à une méta-table, on ne dira pas que __index est une clé mais on dira que __index est un champ. Comme cela, lorsque l’on parlera de champ, on saura que l’on a affaire à une clé d'une méta-table. Attention toutefois, si l’on parle généralement de champ quand il s'agit d'une méta-table, il peut arriver que l’on utilise le mot champ au lieu du mot clé quand il s'agit d'une table ordinaire, mais c’est plus rare.
Ensuite, une question vient tout de suite à l'esprit. Comment sait-on que mt.__index est une méta-méthode concernant le comportement à adopter si l’on essaye d'accéder à une table avec une clé qui n'existe pas. En fait, les champs des méta-méthodes sont déjà préprogrammés dans le Lua. Si l’on utilise une clé qui n'existe pas dans une table, le Lua (avant de retourner erreur de script) va automatiquement commencer par regarder si la table concernée possède une méta-table et, si c’est le cas, va regarder ce qui se trouve, dans cette méta-table, à la clé __index (champ __index). Nous verrons dans un prochain exemple que si nous voulions additionner deux tables, le Lua chercherait automatiquement une méta-table associée ayant un champ se nommant __add.

## Étude des champs d'une méta-table

Nous avons vu précédemment que le Lua avait prédéfini des champs particuliers (comme __index) pouvant être utilisés dans une méta-table pour indexer des méta-méthodes définissant le comportement particulier de la table à laquelle la méta-table est associée. Nous allons, dans ce paragraphe, passer en revue les principaux champs en essayant de donner, pour chacun d'eux, un exemple d'utilisation.
Tous les exemples se trouvent dans le Module:Champ.
Avant de commencer, nous pouvons faire une petite remarque. Il se peut que certaines méta-méthodes ne concernent qu'une seule table. Dans ce cas-là, il n’est pas nécessaire de placer la méta-méthode dans une autre table que la table concernée. On peut placer la méta-méthode dans la table concernée en la déclarant comme étant sa propre méta-table. Par exemple, pour déclarer que la table a est sa propre méta-table, on écrira setmetatable(a,a).

#### Le champ __index

Nous avons commencé à décrire ce champ sommairement plus haut pour simplifier l'exposé. Nous y revenons ici pour compléter notre description. Nous avons vu que la méta-méthode indexée par ce champ était une fonction. Dans l'exemple que nous avons donné, la fonction en question n'avait pas d'arguments.
Toutefois, il est possible d'y rajouter des arguments. Supposons que, dans le message que la méta-méthode retourne, nous voulions donner plus de précision au sujet de la clé qui n'existait pas dans la table. Il nous faut alors passer en argument la table ainsi que la clé fautive (la table est nécessaire car une méta-table peut être associée à plusieurs tables).
Le programme vu précédemment peut ainsi être amélioré :
tab reçoit le contenu de la table (par référence) et est donc de type table. cle reçoit la clé qui a posé problème. Nous pouvons donc, dans le retour, spécifier la clé qui a posé problème.
En tapant {{#invoke:Champ|vindex}}, on obtient : a la clé 1, on trouve : Piano a la clé 2, on trouve : Boule a la clé 3, on trouve : Tortue a la clé 4, on trouve : Nénuphar a la clé 5, on trouve : (la clé 5 n'existe pas dans cette table.)
* Question : Supposons que l’on ait prévu une méta-méthode en cas d'accès à une table avec un index qui n'existe pas et supposons qu’à titre exceptionnel, on souhaite que l'accès à la même table avec une clé qui n'existe pas retourne à nouveau nil dans un cas particulier. Est-ce possible ?
* Réponse : Oui, c’est possible il existe une fonction rawget telle que rawget(tab,k) est équivalent à tab[k] sauf que pour la fonction rawget, la méta-méthode est ignorée. On pourra donc utiliser cette fonction si l’on veut que l'accès à une table avec un index inexistant se comporte comme si l’on n'avait pas de méta-table.

#### Le champ __add

Nous allons voir dans ce paragraphe comment additionner deux tables. Supposons que l’on ait deux tables B et C contenant les coordonnées de deux points B et C et supposons que nous voulions trouver un point A dont les coordonnées (mémorisées dans une table A) seront la somme des coordonnées de B et C. Par exemple, si B a pour coordonnées (3,7) et C a pour coordonnées (1,9), alors les coordonnées de A devront être (4,16).
Bien sûr, une façon simple de faire cela serait d'écrire :
et ça marche !
Le problème, c’est que l’on risque d’avoir toute une suite de calculs longs et fastidieux à faire et l’on aimerait bien simplifier l'écriture du programme ainsi :
Mais là, nous obtenons une erreur de script car B + C n’est pas défini a priori !
Que faire ? En fait, c’est très simple. Nous allons tout d’abord déclarer une table que nous appellerons point. Nous allons ensuite rattacher cette table aux tables B et C en tant que méta-table grâce aux instructions setmetatable(B,Point) et setmetatable(C,Point). On remarque au passage l'aspect naturel de ces deux déclarations car cela revient à définir A et B comme étant des points. Ensuite, nous allons mettre dans la table point une méta-méthode indiquant comment additionner les tables ayant la table point comme méta-table. Autrement dit nous allons définir dans la table point une fonction Point.__add.
Nous compléterons donc le programme précédent ainsi :
En tapant {{#invoke:Champ|vadd}}, on obtient : Le point A à pour coordonnées 4 et 16
Nous voyons que ça marche ! Nous n'avons plus d'erreur de script !

#### Le champ __sub

Ce champ permet de décrire comment faire la différence de deux tables. L'utilisation est similaire au champ __add, il suffit de remplacer + par -. Nous écrirons :
En tapant {{#invoke:Champ|vsub}}, on obtient : Le point A à pour coordonnées 2 et -2

#### Le champ __mul

Ce champ permet de décrire comment multiplier deux tables entre elles. Si on n'avait guère de choix quand il s'agissait d'additionner ou de soustraire deux tables, la situation est différente pour la multiplication. En effet, ne serait-ce que dans le domaine des mathématiques, on peut envisager plusieurs façons de multiplier deux tables. Si les tables contiennent les coordonnées des vecteurs, on peut envisager d’en faire le produit scalaire ou le produit vectoriel. Si les tables mémorisent des matrices, on peut envisager d’en faire le produit matriciel. Plus généralement, si les tables sont des tenseurs, on peut faire des produits tensoriels. Nous voyons que la notion de méta-méthode acquiert une certaine importance dans le cas de la multiplication de deux tables. Dans ce paragraphe, nous verrons comment programmer une méta-méthode faisant le produit scalaire de deux vecteurs (nous étudierons le produit vectoriel et le produit matriciel en exercice).
* Le produit scalaire de deux vecteurs de coordonnés (a,b) et (c,d) est le nombre ac+bd. Autrement dit c’est la somme du produit des abscisses et du produit des ordonnées.
Nous allons donc créer une méta-méthode réalisant cette opération.
Dans ce dernier programme, nous remarquons une particularité intéressante, c’est que le produit de deux tables ne nous a pas donné une table mais un nombre. Avec les méta-méthodes, nous pouvons faire en sorte qu'une opération entre deux tables nous donne quelque chose qui n’est pas une table.
En tapant {{#invoke:Champ|scalaire}}, on obtient : Le produit scalaire des vecteurs B et C est 66

#### Le champ __div

Nous serions tenté de dire : "Dans ce paragraphe, nous allons voir comment diviser deux tables". En fait, il est rare d’avoir à diviser deux tables entre elles. Par conséquent, nous allons plutôt voir dans ce paragraphe comment diviser une table par un nombre, ce qui est plus utile comme par exemple dans l'opération vue plus haut dans ce chapitre.
$I={\frac {B+C}{2}}$
C'est-à-dire que nous allons enfin voir, à titre d'exemple, comment calculer le milieu de deux points.
En tapant {{#invoke:Champ|milieu}}, on obtient : Le milieu des deux points B et C a pour coordonnées 2 et 8
* Nous avons écrit :
* local I =
* local S =
* S = B + C
* : setmetatable(S,Point)
* I = S/2
* Alors que le bon sens nous aurait plutôt poussé à écrire :
* local I =
* local S =
* setmetatable(S,Point)
* S = B + C
* I = S/2
* Pourtant, cette dernière façon d'écrire le programme provoque une erreur de script comme si l'opération S = B + C faisait perdre le fait que S a pour méta-table Point.
* S'agit-il d'un bug dans le Lua ou Scribunto ? Affaire à suivre !

#### Le champ __concat

Dans ce paragraphe, nous allons voir comment concaténer deux tables. Là aussi, nous pouvons imaginer plusieurs façons de concaténer deux tables. Nous allons en voir une ici, à titre d'exemple, et nous en verrons une autre dans la page d'exercices associée.
En tapant {{#invoke:Champ|concatene}}, on obtient : En concatènant les deux tables B et C, avons obtenu : (31,79).

#### Le champ __pow

Ce champ nous permet d'accéder à une méta-méthode permettant d'élever une table à une certaine puissance. Pour simplifier, l'exemple ci-dessous se contente d'élever chaque nombre de la table à la puissance indiquée. On pourrait, pour compliquer, imaginer une méta-méthode qui élève une matrice carrée à une certaine puissance entière.
En tapant {{#invoke:Champ|puissance}}, on obtient : En élevant la table B à la puissance 2, nous obtenons : (9,49).

#### Le champ __mod

La fonction modulo donne le reste de la division par un nombre donné. L'exemple ci-dessous nous donne une table contenant les restes des divisions des nombres de la table donnée par un nombre donnée.
En tapant {{#invoke:Champ|modulo}}, on obtient : En calculant la classe modulo 3 de la table B, nous obtenons : (0,1).

#### Le champ __unm

Ce champ nous fournit une méta-méthode pour interpréter ce que peut donner une table affectée du signe -. Dans notre exemple, on obtiendra une table ou les signes respectifs de toutes ses valeurs sont inversés.
En tapant {{#invoke:Champ|negation}}, on obtient : En calculant l'opposée de la table B, nous obtenons : (-3,-7).

#### Le champ __eq

La comparaisons a == b est valable si a et b sont des tables. a == b retournera true si la table a contient les mêmes éléments que la table b. Mais on peut souhaiter un autre comportement.
Ce champ nous permet d'interpréter ce que peut donner l'opérateur relationnel == placé entre deux tables. Par exemple, comme c’est le cas dans notre exemple, on peut vouloir tester l'égalité approximative entre deux tables. Il y a une différence entre ce champ et les champs vus précédemment, c’est que les deux tables concernées doivent avoir la même méta-méthode dans leur méta-table. Le plus simple est en fait qu’elles aient la même méta-table. Il en sera de même pour les autres opérateurs relationnels <, >, <=, >=.
En tapant {{#invoke:Champ|egal}}, on obtient : Les tables B et C sont différentes.
Comme pour le champs __index, on peut souhaiter que la méta-méthode soit ignorée dans certain cas. On utilisera alors la fonction rawequal utilisant deux paramètres sous la forme rawequal(a,b) qui est équivalent à a == b mais qui ignore la méta-méthode de ce paragraphe.

#### Le champ __le

Ce champ nous permet d'interpréter ce que peuvent donner les opérateurs relationnels >= et <= placés entre deux tables. En principe, comme c’est le cas dans notre exemple, il s'agit de tester si une table est inférieure ou égale ou supérieure ou égale à l'autre. Toutefois, comme il y a plusieurs nombres dans la table, on peut avoir le choix sur la méta-méthode. Dans notre exemple, on dira qu'une table est supérieure ou égale à une autre table si la somme des nombres appartenant à la première table est supérieure ou égale à la somme des nombres appartenant à la deuxième table. Il y a une différence entre ce champ et les champs non relationnels, c’est que les deux tables concernées doivent avoir la même méta-méthode dans leur méta-table. Le plus simple est en fait qu’elles aient la même méta-table. Il en sera de même pour les autres opérateurs relationnels <, >, ==.
En tapant {{#invoke:Champ|superieur}}, on obtient : La table B est supérieure ou égale à la table C.

#### Le champ __lt

Ce champ nous permet d'interpréter ce que peuvent donner les opérateurs relationnels > et < placés entre deux tables. En principe, comme c’est le cas dans notre exemple, il s'agit de tester si une table est inférieure ou supérieure à l'autre. Toutefois, comme il y a plusieurs nombres dans la table, on peut avoir le choix sur la méta-méthode. Dans notre exemple, on dira qu'une table est inférieure à une autre table si la somme des nombres appartenant à la première table est inférieure à la somme des nombres appartenant à la deuxième table. Il y a une différence entre ce champ et les champs non relationnels, c’est que les deux tables concernées doivent avoir la même méta-méthode dans leur méta-table. Le plus simple est en fait qu’elles aient la même méta-table. Il en sera de même pour les autres opérateurs relationnels <=, >=, ==.
En tapant {{#invoke:Champ|inferieur}}, on obtient : La table B est supérieure ou égale à la table C.

#### Le champ __ipairs

Ce champ permet de donner une alternative à la fonction ipairs. Si ce champ existe, l'appel à la fonction ipairs nous retournera la fonction itérative, la table, et l'instruction d'arrêt prévu par la méta-méthode __ipairs.
Dans l'exemple qui suit, la méta-méthode associée à la fonction ipairs est programmée de façon à sélectionner un objet sur deux dans la table souk. L'appel à la fonction ipairs, dans la fonction p.liste, nous retournera donc une fonction itérative suivant qui incrémente la clé de deux unités au lieu d'une.
En tapant {{#invoke:Champ|liste}}, on obtient : à la clé numéro 1 se trouve l’objet flute. à la clé numéro 3 se trouve l’objet manche à balaie. à la clé numéro 5 se trouve l’objet jeu de cartes. à la clé numéro 7 se trouve l’objet tourne vis. à la clé numéro 9 se trouve l’objet stylo.

#### Le champ __metatable

La grande nouveauté pour ce champ est qu’il n'indexe pas une fonction. Ce champ indexe une valeur qui peut être une chaîne de caractères, un nombre, un booléen ou une table. Dans notre exemple, ce champ indexe la chaîne de caractère "Coordonnées" pour indiquer que la méta-table est associée à des tables qui contiennent des coordonnées. Lorsqu'on utilise la fonction getmetatable sur une table ayant pour méta-table, une table contenant ce champ, au lieu d'obtenir la méta-table de la table, on obtiendra le message "Coordonnées".
En tapant {{#invoke:Champ|obtenir}}, on obtient : Coordonnées

#### Le champ __tostring

Ce champ indexe une méta-méthode indiquant ce que doit donner la fonction tostring si on l'applique à une table (qui a une méta-table contenant la méta-méthode en question). Dans notre exemple, on obtiendra une chaîne de caractères représentant la table formée des deux coordonnées séparées par une virgule et se trouvant entre parenthèses.
En tapant {{#invoke:Champ|convertir}}, on obtient : (3,7)

#### Le champ __call

Ce champ permet de transformer une table en fonction lorsqu'on l'emploie en utilisant la syntaxe d'une fonction. Dans l'exemple ci-dessous, la table tb devient une fonction tb qui ajoute 3 à son argument. tb(17) donne alors 20.
En tapant {{#invoke:Champ|appel}}, on obtient : 20

#### Le champ __newindex

Ce champ permet de rendre possible une assignation dans une table, pour une clé donnée, uniquement s'il y a déjà une valeur pour cette clé. Autrement dit, on peut modifier des valeurs déjà existantes dans la table mais on ne peut pas en ajouter de nouvelles. Dans l'exemple ci-dessous, nous avons prévu deux fonctions p.assigne1 et p.assigne2. Dans la fonction p.assigne1, nous voyons que nous avons déclaré une table tb sans l'initialiser. L'instruction tb[2] = "Framboise" est donc sans effet. Nous le constatons en retournant le type de tb[2] qui est nil indiquant que l'assignation a été inopérante. Dans la fonction p.assigne2, nous voyons que nous avons déclaré une table tb en l'initialisant. L'instruction tb[2] = "Framboise" est donc opérationnelle. Nous pouvons le constater en retournant la valeur de tb[2] qui est Framboise indiquant que l'assignation a été faîte.
En tapant {{#invoke:Champ|assignation1}}, on obtient : nil
En tapant {{#invoke:Champ|assignation2}}, on obtient : Framboise
Comme pour le champs __index, on peut souhaiter que la méta-méthode soit ignorée dans certain cas. On utilisera alors la fonction rawset utilisant trois paramètres sous la forme rawset( tab, k, v ) qui est équivalente à tab[k] = v mais qui ignore la méta-méthode de ce paragraphe.

#### Le champ __mode

Ce champ permet de mettre des références faibles dans une table. Si ce champ est utilisé, l'espace alloué à une valeur de la table peut être récupéré par le ramasse miette. Les valeurs stockées dans la table peuvent ainsi être perdues aléatoirement. Si le champ indexe la chaîne "k", les clés de la table peuvent être nettoyées. Si le champ indexe la chaîne "v", les valeurs de la table peuvent être nettoyées.
Il est difficile de donner un exemple pour ce champ. Nous en resterons donc là !
En tapant {{#invoke:Champ|faible}}, on obtient : table
| Gestion de l'environnement | Librairies Scribunto |