Initiation au Lua avec Scribunto/Tables et fonctions

Début de la boite de navigation du chapitre

Nous allons, dans ce chapitre étudier plus en détail les tables et les fonctions que nous avons entrevues dans les chapitres précédents. Une table, c’est pour ainsi dire, une supervariable qui a la faculté de contenir plusieurs objets, au lieu d'un seul, comme les autres variables. La table est, pour ainsi dire, le point fort du Lua par rapport aux autres langages à cause des possibilités offertes qui sont plus nombreuses que pour les autres langages. En contrepartie, cette notion est plus dure à assimiler pour le Lua que pour les autres langages. Nous allons donc procéder par étapes, dans ce chapitre, en commençant par les notions simples et en compliquant au fur et à mesure. Le lecteur n’est pas obligé de tout assimiler. Comprendre les premières notions lui permettra de traiter les tables en Lua comme elles sont traitées dans les autres langages et cela peut lui être suffisant pour programmer correctement. Nous approfondirons aussi les fonctions. Si nous étudions ces deux notions dans un même chapitre, c’est parce-qu’il y a une certaine interconnexion entre tables et fonctions. En effet, on peut faire des tables de fonctions et les fonctions peuvent recevoir des tables en arguments. Il est donc difficile de décider quoi étudier en premier !

Tables et fonctions
Icône de la faculté
Chapitre no 3
Leçon : Initiation au Lua avec Scribunto
Chap. préc. :Mise au point d'un module
Chap. suiv. :Structures de contrôle

Exercices :

Sur les tables et les fonctions
fin de la boite de navigation du chapitre
En raison de limitations techniques, la typographie souhaitable du titre, « Initiation au Lua avec Scribunto : Tables et fonctions
Initiation au Lua avec Scribunto/Tables et fonctions
 », n'a pu être restituée correctement ci-dessus.

Définition élémentaire d'une table

modifier

Au premier chapitre, nous avons effleuré la définition d'une table lorsque nous avons étudié l'instruction

local p = {}

Mais une table ne s’appelle pas forcément p. On peut lui donner un nom plus parlant sur sa fonction. Par exemple :

local semaine = {}

Et il est possible de l'initialiser :

local semaine = {"lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"}

On aurait pu, tout aussi bien, créer une table de nombres. Par exemple :

local nombres_premiers = {2,3,5,7,11,13,17,19,23,29,31,37,41}

Comment accéder à un élément d'une table. Il suffit d'écrire le nom de la table suivi de la position entre crochets de l’objet auquel on souhaite accéder. Ce genre de table, indexée par la position de l'objet, s’appelle une séquence en Lua.

Par exemple, pour notre première table, semaine[3] représente "mercredi". C'est le troisième jour de la semaine.

Pour notre deuxième table ; nombres_premiers[5] représente le cinquième nombre premier qui est 11.


Prenons un exemple pour illustrer ce que l’on vient de dire :

Écrivons un programme qui traduit un jour de la semaine en anglais. On l'a déjà fait au premier chapitre, mais cette fois écrivons un programme qui à partir seulement d'un nombre, nous donne une phrase indiquant la traduction du jour de position le nombre donné. Par exemple si l’on rentre 4, on doit obtenir la phrase : "La traduction de jeudi, en anglais est thursday" car jeudi est le quatrième jour de la semaine.

Dans le Module:Traduit écrivons :

local p = {}
local semaine = {"lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"}
local week = {"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}

function p.baratin1(frame)
	index = tonumber(frame.args[1])
	return "La traduction de "..semaine[index]..", en anglais, est "..week[index]
end

return p

En écrivant : {{#invoke:Traduit|baratin1|5}}, nous obtenons : La traduction de vendredi, en anglais, est friday


Index évolués

modifier

Nous venons de voir que les index des tables peuvent s'écrire à l'aide de nombres. Mais il est possible aussi de les écrire à l'aide de chaîne de caractères. Reprenons notre Module:Traduit. Nous pouvons alors imaginer un moyen astucieux de traduire un mot en anglais en se servant de ce mot directement comme index d'une table qui contiendrait toutes les traductions. Par exemple, dans une table tab avec l'index "chien", on accéderait à l'emplacement tab["chien"] où se trouverait la chaîne de caractère dog

Sur ce principe, écrivons donc un nouveau programme de traduction des jours de la semaine en anglais :

local p = {}
local sem = { ["lundi"] = "monday", ["mardi"] = "tuesday", ["mercredi"] = "wednesday", ["jeudi"] = "thursday", ["vendredi"] = "friday", ["samedi"] = "saturday", ["dimanche"] = "sunday"}

function p.anglais(frame)
	local jour = frame.args[1]
	return "La traduction de "..jour..", en anglais, est "..sem[jour]
end
return p

En écrivant : {{#invoke:Traduit|anglais|samedi}}, nous obtenons : La traduction de samedi, en anglais, est saturday

Nous remarquons comment la table sem a été pré-remplie. Nous sommes obligés de bien préciser à quel indice correspond quelle information.


Si, dans un programme, nous sommes amenés à accéder directement à une donnée de la table sans passer par le biais d'une variable, nous pouvons l'écrire autrement. Par exemple, au lieu d'écrire sem["mardi"], on peut écrire, plus simplement, sem.mardi.

Pour bien vérifier l'équivalence de deux notations, nous avons rajouté une fonction dans le Module:Traduit :

function p.information(frame)
	return "Le premier jour de la semaine anglaise est "..sem["lundi"].." et le dernier jour est "..sem.dimanche
end

En écrivant : {{#invoke:Traduit|information}}, nous obtenons : Le premier jour de la semaine anglaise est monday et le dernier jour est sunday

Nous avons utilisé les deux notations dans la même phrase pour vérifier qu’elles sont bien équivalente.


Par contre, si dans la fonction p.anglais, nous avions écrit sem.jour au lieu de sem[jour], nous aurions une erreur de script car jour est une variable qui contient une chaîne de caractères, mais n'est pas, elle-même, une chaîne de caractères.

 

Question : Lorsque les index sont des nombres, ne peut-on pas accéder à un élément de la table en écrivant sem.3 au lieu de sem[3].

Réponse : Non, car sem.3 est équivalent à sem["3"], mais pas à sem[3].


La notation que l’on vient de voir se répercute aussi sur la déclaration des tables. C'est-à-dire que la table sem de notre Module:Traduit aurait tout aussi bien pu s'écrire sous la forme simplifiée suivante :

local p = {}
local sem = { lundi = "monday", mardi = "tuesday", mercredi = "wednesday", jeudi = "thursday", vendredi = "friday", samedi = "saturday", dimanche = "sunday"}

function p.anglais(frame)
	local jour = frame.args[1]
	return "La traduction de "..jour..", en anglais, est "..sem[jour]
end
return p

Nous voyons, par exemple, que ["lundi"] a été remplacé par lundi.


 

Ce que nous avons dit n’est pas tout à fait vrai. Si nous sommes obligé d'utiliser, comme index, une chaîne de caractères ayant un accent alors les deux notations ne sont plus équivalentes. Par exemple sem["Nénuphar"] ne peut pas être remplacé par sem.Nénuphar qui sera rejeté par l'éditeur. Il en est de même pour la déclaration des tableaux. Ci-dessus nous avons eu de la chance car aucun des jours de la semaine ne prend d'accent.


Tables de tables

modifier

On peut aussi manipuler des tables de tables, c'est-à-dire une table contenant des tables.

Si, par exemple, on veut déclarer une table contenant quatre tables, on écrira :

local t = {{},{},{},{}}

Mathématiquement, ce genre de table peut correspondre à des matrices.

Par exemple, la matrice :

 

se déclarera :

local A = {{2,1,-4,6},{5,-3,-2,4},{1,3,-4,7},{-5,3,2,5}}

Si l’on veut accéder au nombre -2 se trouvant à la deuxième ligne, troisième colonne, on écrira A[2][3].


Si l’on déclare une table (ou matrice) B ainsi :

local B = {}

Et que l’on écrit dans le programme, par exemple :

B[3][1] = 9

On obtiendra une erreur de script.


Fonctions relatives aux tables

modifier

Dans le chapitre 8, nous étudierons plus en détail d'autres fonctions relatives aux tables, en particulier les fonctions :

  • table.insert(t, ligne) : incrémente la table avec "ligne".
  • table.remove(t,ligne) : retire un élément de la table.
  • table.concat(t, séparateur) : convertit la table en une chaîne de caractères, en séparant chaque ligne par une éventuelle autre chaîne.
  • table.maxn(t) : Retourne le plus grand index numérique positif utilisé dans la table.
  • table.sort(t) : Permet de trier la table.
  • table.getn(t) : renvoie la taille de la table.


Complément sur les fonctions

modifier

Fonctions appelées par une fonction

modifier

Nous avons déjà bien étudié les fonctions. Mais toutes les fonctions que nous avons vues jusqu'à maintenant étaient placées d'office dans une table que nous avons appelée p pour les besoins de #invoke (Ce qui nous montre déjà que l’on peut faire des tables de fonctions). Nous allons voir maintenant qu'une fonction peut exister sans être dans une table. Une fonction peut être appelée simplement par une autre fonction. Prenons un exemple :

Dans le Module:Fonction, écrivons une fonction qui calcule automatiquement les carrés des 4 premiers nombres premiers en prenant soin de mettre à part la fonction qui élève au carré.

local p = {}

function f(x)
	return x^2
end

function p.carre1(frame)
	local reponse = "<u>Nombres premiers élevés aux carrés</u> <br />"
	reponse = reponse.."Le carré du nombre 2 est "..f(2).."<br />"
	reponse = reponse.."Le carré du nombre 3 est "..f(3).."<br />"
	reponse = reponse.."Le carré du nombre 5 est "..f(5).."<br />"
	reponse = reponse.."Le carré du nombre 7 est "..f(7).."<br />"
	return reponse
end

return p

La fonction f se contente d'élever au carré le nombre x, qui représente son argument, et nous voyons que cette fonction est appelée dans la fonction p.carre1 qui se trouve dans la table p.

En écrivant : {{#invoke:Fonction|carre1}}, nous obtenons :

Nombres premiers élevés aux carrés
Le carré du nombre 2 est 4
Le carré du nombre 3 est 9
Le carré du nombre 5 est 25
Le carré du nombre 7 est 49

Paramètres et valeurs retournées par une fonction

modifier

Les fonctions peuvent recevoir plusieurs paramètres. Pour passer plusieurs paramètres à une fonction, il suffit de les écrire simplement en les séparant par des virgules. Par exemple :

function f(x,y,z)
	return x^2+y^2+z^2
end

Plus remarquable encore, une fonction peut retourner plusieurs valeurs en les séparant par des virgules :

function f(x)
	return x^2,2x,5x-3
end

Nous voyons que la forme que prennent les valeurs en sortant est similaire à la forme que prennent les paramètres à l'entrée. Nous pouvons mettre ceci à profit en emboîtant des fonctions (fonctions composées). Prenons un exemple pour voir si cela marche bien :

local p = {}

function g(x,y,z)
	return 2*x+y+3*z
end

function h(x)
	return x,2*x,x
end

function p.composition(frame)
	return g(h(frame.args[1]))
end

return p

Nous voyons que la fonction h a un seul paramètre mais retourne trois valeurs. La fonction g, qui traite trois paramètres, peut recevoir les trois valeurs retournées par h et nous renvoie une valeur. Nous pouvons vérifier que ça marche bien avec les deux exemples suivants :


En écrivant : {{#invoke:Fonction|composition|5}}, nous obtenons : 35

En écrivant : {{#invoke:Fonction|composition|3}}, nous obtenons : 21


Il nous reste tout de même un petit problème : Commet récupérer les valeurs retournées par une fonction qui retourne plusieurs valeurs. C'est là que nous allons utiliser l'affectation simultanée de plusieurs variables que nous avons entrevue à la fin du premier chapitre. Supposons que f soit une fonction qui retourne trois valeurs par exemple et que nous voulions récupérer les valeurs retournées pour f(3) par exemple. Nous écrirons tout simplement:

a,b,c = f(3)

et les trois valeurs retournées par f seront respectivement dans les trois variables a, b, c.

Une question vient à l'esprit : Et si nous ne sommes intéressés que par la première et la troisième valeur ? Dans ce cas nous écrirons :

a,,c = f(3)

Et si nous ne sommes intéressé que par la première valeur ? Dans ce cas, nous écrirons :

a = f(3)

Et si nous ne sommes intéressé que par la troisième valeur ? Dans ce cas, nous écrirons :

,,c = f(3)

Et si nous ne sommes intéressé que par les deux premières valeurs ? Dans ce cas, nous écrirons :

a,b = f(3)

Et ainsi de suite !!

Fonctions récursives

modifier

En Lua, comme en C ou en Pascal (mais pas en Fortran, ni en Cobol), les fonctions peuvent s'appeler elles-mêmes. On appelle ce phénomène la récursivité. Prenons l'exemple classique de la fonction factorielle.

local p = {}

function fact(n)
    if n == 0 then
        return 1 -- on renvoie la valeur 1 quand le paramètre vaut 0
    else
        return n * fact(n - 1)
    end
end

function p.factorielle(frame)
	return fact(frame.args[1])
end

return p

La fonction récursive est la fonction fact qui a pour argument n et qui fait appel à fact(n-1) si n est différent de 0. Cette fonction va s'appeler elle-même jusqu'à ce que son argument soit nul. Par exemple, si l’on veut calculer factorielle de 4 que l’on note 4!, on aura, en suivant le cheminement de la fonction :

4! = 4✕3! = 4✕3✕2! = 4✕3✕2✕1! = 4✕3✕2✕1✕0! = 4✕3✕2✕1✕1 = 4✕3✕2✕1 = 24


En écrivant : {{#invoke:Calcul|factorielle|5}}, nous obtenons : 120 car 120 = 5✕4✕3✕2✕1

 

Question : Dans le programme précédent, n'aurait-on pas pu rendre directement la fonction p.factorielle récurssive au lieu de lui faire appeler une autre fonction récursive, ici la fonction fact ?

Réponse : Non, car la fonction p.factorielle a pour argument frame et est donc, par conséquent, dédiée à recevoir des informations de l'extérieur du module et pas de l'intérieur. Elle n'est donc pas appelable de l'intérieur du module et donc ne peut pas s'appeler elle-même.


Tables de fonctions

modifier

En lua, nous pouvons créer des tables de fonctions. Nous devrions commencer à y être habitués car depuis le début de cette leçon, nous utilisons une table que nous avons appelée p (mais qui pourrait s'appeler autrement) dans laquelle, nous rangeons nos fonctions. Dans ce paragraphe, nous allons essayer de bien clarifier cette notion que nous utilisons, peut-être, mécaniquement sans trop bien comprendre ce que nous faisons. Pour cela nous allons utiliser des exemples.

Nous attirons l'attention du lecteur sur le fait que les exemples qui suivent (comme la plupart des exemples de cette leçon) pourront paraître totalement loufoques au programmeur de formation. Ils ont uniquement pour but de bien faire assimiler la notions de tables de fonctions aux étudiants et n'ont, par contre, aucune valeur d'exemple sur la manière de résoudre un problème concret.

Exemple1

Nous allons écrire dans un Module:Ajout, trois fonctions f,g,h ayant pour but d'ajouter respectivement 1, 2, 3 à son argument. Nous écrirons ensuite une fonction p.ajoute qui, dans un premier temps, va ranger les trois fonctions f, g, h dans une table et ensuite, dans un deuxième temps, va incrémenter son premier argument, de la valeur indiquée par son deuxième argument, en utilisant les fonctions rangées dans la table :

local p = {}

function f(x)
	return x+1
end

function g(x)
	return x+2
end

function h(x)
	return x+3
end

function p.ajoute(frame)
	local Aj = {}
	local reponse = tonumber(frame.args[1])
	Aj.ajoute1 = f
	Aj.ajoute2 = g
	Aj.ajoute3 = h
	if frame.args[2] == "1" then reponse = Aj.ajoute1(reponse) end
	if frame.args[2] == "2" then reponse = Aj.ajoute2(reponse) end
	if frame.args[2] == "3" then reponse = Aj.ajoute3(reponse) end
	return reponse
end

return p

En écrivant : {{#invoke:Ajout|ajoute|17|2}}, nous obtenons : 19


Exemple2

Nous avons vu, au début de ce chapitre, que la notation Aj.ajoute1 utilisée pour les tables était une façon de noter, plus simplement, un accès à une table indexée par des chaînes de caractères et qui se noterait plus logiquement Aj["ajoute1"]. Nous allons donc, à titre d'exemple 2, rajouter, dans le Module:Ajout, une fonction p.rajoute qui est l'exacte réplique de la fonction p.ajoute mais utilisant l'autre notation pour l'accès a la table de fonctions Aj

function p.rajoute(frame)
	local Aj = {}
	local reponse = tonumber(frame.args[1])
	Aj["ajoute1"] = f
	Aj["ajoute2"] = g
	Aj["ajoute3"] = h
	if frame.args[2] == "1" then reponse = Aj["ajoute1"](reponse) end
	if frame.args[2] == "2" then reponse = Aj["ajoute2"](reponse) end
	if frame.args[2] == "3" then reponse = Aj["ajoute3"](reponse) end
	return reponse
end

En écrivant : {{#invoke:Ajout|rajoute|23|1}}, nous obtenons : 24

Nous voyons donc que la notation Aj.ajoute1 est bien équivalente à la notation Aj["ajoute1"]


Pourquoi avons nous pris la peine de donner ces deux exemples identiques, à la notation de l'accès à la table près. C'est pour que l'étudiant prenne bien conscience que, dans la notation Aj.ajoute1, nous n'avons pas une fonction qui s'appellerait ajoute1 et qui serait placée dans la table Aj (comme certains pourraient le croire) mais nous avons une table de fonctions indexée par des chaînes de caractères. ajoute1 représente la chaîne de caractère "ajoute1" qui sert d'index d'accès à une fonction se trouvant dans la table Aj.

À l'appui de ce que nous venons de dire, on pourrait souligner le fait que puisque ajoute1 représente une chaîne de caractères et pas une fonction, nous aurions pu créer à part une vraie fonction ajoute1 et il n'y aurait pas eu de conflit. C'est ce que l’on aurait pu faire un peu plus haut lorsque nous avons donné l'exemple de la fonction factorielle comme fonction récursive. Nous avions écrit pour éviter d'embrouiller les esprit :

local p = {}

function fact(n)
    if n == 0 then
        return 1 -- on renvoie la valeur 1 quand le paramètre vaut 0
    else
        return n * fact(n - 1)
    end
end

function p.factorielle(frame)
	return fact(frame.args[1])
end

return p

Mais nous aurions pu écrire :

local p = {}

function factorielle(n)
    if n == 0 then
        return 1 -- on renvoie la valeur 1 quand le paramètre vaut 0
    else
        return n * factorielle(n - 1)
    end
end

function p.factorielle(frame)
	return fact(frame.args[1])
end

return p

et cela aurait tout aussi bien fonctionné.


Exemple3

La notation Aj.ajoute1 semble plus simple que la notation Aj["ajoute1"]. Nous allons donc dans cet exemple 3, mettre en évidence un intérêt, que peut avoir la dernière notation, en simplifiant l'exemple 2.

Dans le Module:Ajout, nous rajouterons la fonction p.incremente qui est une simplification de la fonction p.rajoute mettant à profit le fait que l’on a accès à l'index sous forme de chaîne de caractères. Nous écrirons :

function p.incremente(frame)
	local Aj = {}
	local index = "ajoute"..frame.args[2]
	local reponse = tonumber(frame.args[1])
	Aj["ajoute1"] = f
	Aj["ajoute2"] = g
	Aj["ajoute3"] = h
	reponse = Aj[index](reponse)
	return reponse
end

En écrivant : {{#invoke:Ajout|incremente|47|3}}, nous obtenons : 50



Fonctions ayant une table pour argument

modifier

Une fonction peut recevoir en argument tout type d'objet. Par conséquent, une fonction peut recevoir une table en argument. Il y a toutefois une petite différence dans le passage d'une table en argument dans une fonction. Les tables sont passés par référence alors que la plupart des autres variables sont passées par valeur. Nous allons nous efforcer de bien comprendre ce que cela signifie dans la suite de ce paragraphe.

Lorsqu'on passe un objet par valeur à une fonction, cela signifie que la fonction recopie l’objet dans la variable déclarée en argument dans la fonction. par exemple reprenons la fonction f défini plus haut :

function f(x)
	return x^2
end

Si dans une autre fonction, on écrit

resultat = 7+f(a)

À l'appel de la fonction f par f(a), la valeur de a est recopié dans la variable x définie dans la fonction f et c’est x qui va être élevée au carré et pas la variable a.


Par contre, lorsqu'on passe un objet par référence à une fonction, cela signifie que la fonction reçoit l'adresse de l’objet qui lui est passé et pas sa valeur. Par conséquent la fonction va agir directement sur l’objet du programme appelant et pas sur une recopie de l'objet.

Dans le Lua, les tables sont des variables passées par référence. Cela peut se comprendre dans la mesure où les tables peuvent être des objets énormes puisque pouvant contenir un grand nombre d'objets. Si l’on devait recopier la table dans la fonction à chaque appel de fonction, la perte de temps ainsi que l'occupation mémoire serait trop importante.

Nous allons prendre deux exemples pour mettre en évidence la différence entre le passage par valeur et le passage par référence :

Dans un Module:Passage, nous écrirons deux fonctions p.valeur et p.reference. La première, appelant une fonction val auquel elle passe une variable par valeur (ici un nombre). La deuxième appelant une autre fonction ref auquel elle passe une variable par référence (ici une table). Chacune des deux fonctions va ensuite modifier l’objet passé. Nous vérifierons ensuite, dans le programme appelant, si la modification s'est aussi répercuté sur l’objet passé :

local p = {}

function val(x) -- x est sensé être un nombre
	x = x + 3 -- On essaye d'incrémenter de 3 le contenu de x
end

function ref(x) -- x est sensé être une table
	x[1] = x[1] + 3 -- On essaye d'incrémenter de 3 la première valeur de la table
end

function p.valeur(frame)
	local a = tonumber(frame.args[1]) -- a est déclaré comme nombre et est initialisé avec la valeur de l'argument
	val(a) -- appel de la fonction, ici a contient un nombre
	return a -- On retourne le contenu de a pour voir s'il a été modifié
end

function p.reference(frame)
	local a = {tonumber(frame.args[1])} -- a est déclaré comme table et est initialisé avec la valeur de l'argument en a[1]
	ref(a) -- appel de la fonction, ici a contient une table
	return a[1] -- On retourne le contenu de a pour voir s'il a été modifié
end

return p

En tapant {{#invoke:Passage|valeur|37}}, nous obtenons : 37. Nous voyons que l'argument n'a pas été modifié.


En tapant {{#invoke:Passage|reference|37}}, nous obtenons : 40. Nous voyons que l'argument a été incrémenté de 3.


Visibilité d'une variable

modifier

Nous avons, jusqu'à présent toujours déclaré les variables locales en début de fonction ou en début de bloc. La question qui se pose ici est de savoir ce qui se passe si la variable n’est pas déclarée localement en début de bloc, mais au milieu par exemple. Que les utilisateurs soient d'ores et déjà rassurés, cela ne provoque pas une erreur de script. En fait, une variable ne pourra être utilisée localement qu’à partir du moment où elle est déclarée comme locale. Si on tente d’utiliser ou de modifier une variable avant de la déclarer, on utilisera ou l’on modifiera une autre variable de même nom, celle-ci étant globale ou éventuellement déclarée locale en début de module.