Retour au cours

games / lua

Fonctions, portée et closures

Leçon 51 exercice

Explication

Les fonctions comme valeurs, pas juste comme instructions

En Lua, une fonction n'est pas un simple bloc d'instructions nommé : c'est une VALEUR comme un nombre ou une chaîne, qu'on peut stocker dans une variable, passer en paramètre à une autre fonction, ou retourner comme résultat. Cette idée, appelée "fonction de première classe", est ce qui rend possible tout le reste de la leçon.

Ce que vous allez apprendre

  • Déclarer une fonction et la manipuler comme une valeur (l'assigner, la passer en argument)
  • Utiliser les varargs (...) pour accepter un nombre variable d'arguments
  • Exploiter les valeurs de retour multiples, une particularité forte de Lua
  • Construire une closure et comprendre ce qu'elle "capture" réellement
  • Écrire une fonction récursive locale sans tomber dans le piège du nom non résolu

Dans quel contexte ?

Un système d'effets de statut dans un jeu (poison, brûlure, ralentissement) doit générer une fonction différente pour chaque type d'effet, chacune se souvenant de sa propre intensité et de sa propre durée, sans jamais mélanger les états de deux effets actifs en même temps sur des joueurs différents. C'est exactement le problème que les closures résolvent naturellement, et que ce cours retrouvera constamment sous forme de callbacks dans les leçons suivantes.

ConceptCe qu'il permetExemple
Fonction de 1ère classeStocker/passer une fonction comme une valeurlocal f = print
Varargs ...Accepter un nombre variable d'argumentsfunction sum(...)
Retours multiplesRenvoyer plusieurs valeurs distinctesreturn min, max
ClosureSe souvenir de son environnement de créationmakeCounter()

Bonne pratique

Utilise toujours local function nom(...) plutôt que local nom = function(...) end dès qu'une fonction doit s'appeler elle-même (récursion) : au moment de la définition, local function rend le nom immédiatement visible dans son propre corps, contrairement à l'autre syntaxe.

Des fonctions plus flexibles qu'il n'y paraît

Une fonction Lua peut accepter un nombre variable d'arguments (..., les varargs) et surtout peut RETOURNER plusieurs valeurs en une seule fois — pas un tuple ou une liste comme dans d'autres langages, mais littéralement plusieurs valeurs distinctes que l'appelant peut capturer séparément. C'est une particularité du langage qui simplifie beaucoup de code une fois assimilée.

Comprendre les closures avec une image simple

Une closure, c'est une fonction qui "se souvient" de l'environnement dans lequel elle a été créée, même après que cet environnement a normalement cessé d'exister. Imaginez une fonction qui crée un compteur : chaque appel à cette fonction créatrice produit un NOUVEAU compteur indépendant, avec sa propre mémoire privée, parce que chaque closure capture sa propre copie de la variable locale au moment de sa création.

Un piège classique... qui n'existe pas en Lua

Dans certains langages, une boucle for qui crée des closures capture accidentellement la MÊME variable partagée entre toutes les itérations, causant un bug notoire. En Lua, chaque itération d'une boucle for numérique crée une NOUVELLE variable locale distincte — ce piège classique n'existe donc simplement pas, un détail rassurant à connaître.

La récursion, un cas particulier à ne pas rater

Pour qu'une fonction puisse s'appeler elle-même par son nom de façon fiable, il faut la déclarer avec local function nom(...) plutôt que local nom = function(...) end — sinon, au moment où la fonction est définie, son propre nom n'existe pas encore dans la portée locale, et l'appel récursif échouerait ou référencerait la mauvaise variable.

Commandes & code

Fonctions, portée et closures

lua
-- Déclaration de fonction : les fonctions sont des valeurs de première classe (first-class citizens)
local function add(a, b)
  return a + b
end

local multiply = function(a, b)   -- forme équivalente, fonction anonyme assignée à une variable
  return a * b
end

print(add(3, 4), multiply(3, 4))   -- 7  12
lua
-- Nombre variable d'arguments : varargs "..."
local function sum(...)
  local total = 0
  for _, value in ipairs({...}) do   -- {...} capture tous les arguments dans une table
    total = total + value
  end
  return total
end

print(sum(1, 2, 3, 4, 5))   -- 15

-- select() pour inspecter les varargs sans les empaqueter dans une table
local function countArgs(...)
  return select("#", ...)     -- nombre total d'arguments, y compris les nil explicites
end
print(countArgs(1, nil, 3))    -- 3
lua
-- Valeurs de retour multiples, une particularité forte de Lua
local function minMax(t)
  local min, max = t[1], t[1]
  for _, v in ipairs(t) do
    if v < min then min = v end
    if v > max then max = v end
  end
  return min, max                -- deux valeurs retournées
end

local lo, hi = minMax({5, 2, 9, 1, 7})
print(lo, hi)   -- 1  9
lua
-- Closure : une fonction interne "capture" les variables locales de son environnement englobant
local function makeCounter()
  local count = 0                 -- variable capturée par la closure ci-dessous
  return function()
    count = count + 1              -- chaque appel modifie la MÊME variable capturée
    return count
  end
end

local counter1 = makeCounter()
local counter2 = makeCounter()      -- environnement indépendant, sa propre variable "count"
print(counter1())   -- 1
print(counter1())   -- 2
print(counter2())   -- 1 (indépendant de counter1)
lua
-- Cas d'usage réel : générateur d'event handlers paramétrés, pattern très courant en gamedev
local function createDamageHandler(damageAmount)
  return function(entity)
    entity.hp = entity.hp - damageAmount
    print(entity.name .. " subit " .. damageAmount .. " dégâts, HP restant: " .. entity.hp)
  end
end

local fireballHandler = createDamageHandler(50)
local slapHandler = createDamageHandler(5)

local player = { name = "Alice", hp = 100 }
fireballHandler(player)   -- "Alice subit 50 dégâts, HP restant: 50"
slapHandler(player)        -- "Alice subit 5 dégâts, HP restant: 45"
lua
-- Piège classique des closures dans une boucle "for" avec des fonctions accumulées
local handlers = {}
for i = 1, 3 do
  handlers[i] = function() print(i) end   -- en Lua, "i" est une NOUVELLE variable locale à chaque itération
end
handlers[1]()   -- 1
handlers[2]()   -- 2
handlers[3]()   -- 3
-- (contrairement à certains langages où toutes les closures partageraient la même variable finale)
lua
-- Fonctions récursives locales : nécessite "local function" (pas juste "local x = function")
-- pour que la fonction puisse s'appeler elle-même par son nom
local function factorial(n)
  if n <= 1 then return 1 end
  return n * factorial(n - 1)   -- "factorial" doit déjà être visible dans son propre corps
end
print(factorial(6))   -- 720

Résumé

  • Les fonctions Lua acceptent des varargs (...) et retournent nativement plusieurs valeurs.
  • Une closure capture les variables locales de son environnement de création, indépendamment entre instances.
  • Dans une boucle for numérique, chaque itération crée une nouvelle variable locale (pas de piège de partage).
  • local function nom(...) (et non local nom = function) est requis pour une récursion propre par nom.

Exercices pratiques

1 disponible
1

Mission : des effets de poison qui se mélangent entre joueurs

Objectif : Corriger un système d'effets de statut où tous les joueurs empoisonnés semblent partager la même intensité, et fiabiliser une fonction récursive.

Contexte

Un système d'effets de statut RP génère un gestionnaire d'effet par type, mais un développeur a écrit ce code qui pose problème :

lua
local activeIntensity = 0   -- variable globale au fichier, PAS locale à chaque effet

local function makePoisonEffect(intensity)
  activeIntensity = intensity
  return function(entity)
    entity.hp = entity.hp - activeIntensity
  end
end

local poisonWeak = makePoisonEffect(5)
local poisonStrong = makePoisonEffect(20)

poisonWeak(playerA)     -- inflige 20 dégâts au lieu de 5 !

Le développeur pensait, comme dans la leçon sur les closures, que chaque appel à makePoisonEffect créerait un environnement indépendant. Ce n'est visiblement pas le cas ici.

Résoudre l’exercice →