Az alprogramok absztrakciós eszközök a programozási nyelvekben. Az
alprogramoknak két fontosabb fajtája létezik: az eljárások (nincs visszatérési
érték; a Luában az utasítás absztrakciója) és a függvények (van visszatérési
értékük, a kifejezések absztrakciói). A Lua ezek közül a függvényeket támogatja,
habár az alprogram megvalósítható olyan speciális függvényként, aminek a
törzsében nem adunk meg visszatérési értéket. Ezek a függvények automatikusan
nil
értéket adnak vissza, még akkor is, ha a függvény törzsében nem
szerepel return
utasítás. Szintén az eljáráshívást támogatja, hogy a
függvényeket nem csak kifejezésben, hanem utasításként is meg lehet hívni,
ilyenkor a visszatérési érték elveszik.
A függvények érték szerinti paraméterátadást használnak nil
,
boolean
, number
és string
típusnál.
Minden más típus referencia szerint adódik át. Nincs külön
be- vagy kimenõ típusú paraméter. A nyelv egy speciális jellemzője, hogy a fordító
nem ellenőrzi, hogy a formális és aktuális paraméterek száma megegyezik-e. Így ha
kevesebb paramétert adunk meg, akkor a hiányzó értékek nil-lel lesznek
egyenlőek; több paraméter esetén a felesleges paramétereket figyelmen kívül
hagyja a fordító. Ez a mechanizmus lehetőséget ad alapértelmezett paraméterek
megvalósítására is, hiszen ha az aktuális érték nil-lel egyenlő, akkor a
függvényből adhatunk értéket a paraméterváltozónak (a paraméterek
megfeleltetése sorrendjük alapján történik). Asszociatív tömbökkel szimulálható
a név szerinti paraméterátadás.
A függvények egynél több értékkel is visszatérhetnek. Ezt a többszörös
értékadás teszi lehetővé. A return
utasítás csak egy blokk végén
helyezkedhet el, de persze do return end
formában bárhol elhelyezhető
a függvénytörzsön belül.
Az alprogramok function
típusú értékek a nyelvben. Egy ilyen értéket a
következő kifejezéssel lehet létrehozni:
Ezt értékül lehet adni változónak, át lehet adni paraméterként, teljes értékű Lua objektumként működik. Függvények definiálására van egy egyszerűsítés, a következő két utasítás ugyanazt jelenti:
Mindkét esetben egy függvény típusú változó jön létre, ennek a változónak a segítségével lehet meghívni a függvényt. Mindkét formában akár táblamezőnek is lehet értéket adni (table.func alakú nevet lehet megadni), sőt a függvényhívásnál bemutatott kettőspontos rövidítést is lehet használni objektumok metódusainak definiálására. A következő két utasítás megegyezik:
A metódus tehát egy olyan függvény, aminek az első paraméterét a
self
nek hívják. A kettőspontos hívási technikával összekötve ez tényleg
olyan, mintha objektumokhoz definiálnánk metódusokat.
Mivel a függvények első osztályú értékek, azokat nem csak globális változókban, hanem táblamezőkben és lokális változókban is tárolhatjuk:
Ha egy függvényt egy lokális változóban tárolunk lokális függvényt kapunk, amelynek láthatósága egy adott lexikális egységre korlátozódik. Az ilyen definíciók különösek hasznosak csomagok esetén: a Luában minden chunk (ld.: Lua 5.1 Reference Maual, pongyolán: szkript, fordítási egység) deklarálhat lokális függvényeket, amik csak a chunkon belül láthatók.
A lokális függvények definiálására szintaktikus cukor is van a nyelvben:
Rekurzív lokális függvények definíciójánál problémába ütközhetünk. A naiv megközelítés ilyenkor nem működik:
Amikor a Lua lefordítja a fact(n-1)
hívást a függvény törzsében, a lokális
fact
változó még nem definiált. Ezért a kifejezés egy globális változóra hivatkozik
helyette. Ennek elkerülésére először deklarálnunk kell a lokális változót, és csak ezután, külön
utasításban definiálni a függvényt.
A lokális függvényeket definiáló szintaktikus cukor használatakor viszont nem kell aggódnunk emiatt:
A lexikális láthatósági szabályok szerint, ha egy függvényt egy másik függvény törzsében definiálunk, az hozzáfér a befoglaló függvény lokális változóihoz. Az első osztályú függvényekkel kombinálva ez a tulajdonság érdekes technikákat tesz lehetővé.
Vegyünk egy példát, amely mindkettőt tartalmazza:
Az anonim függvény belsejében az i
nem globális és nem is lokális változó. Külső
lokális változónak számít, más néven felső értéknek (upvalue, ld:
Lua 5.1 Reference Manual fordítás). A függvény
az i
-t használja számlálóként, bár az azt létrehozó függvény már visszatért.
A lezártak (closure) használata teszi lehetővé, hogy a Lua helyesen kezelje a helyzetet. Egyszerűen fogalmazva, egy lezárt egy első osztályú függvényt és az összes felső értékét foglalja magában.
Ha újra meghívjuk a newCounter
függvényt, új lokális i
változót
hoz létre, így egy új lezártat kapunk:
A c1
és c2
tehát különböző lezártjai ugyanannak a függvénynek, és
mindkettő az i
egy külön példányával rendelkezik. Szigorúan véve, a Luában nem a
függvények, hanem a lezártak az értékek. A függvény maga csak lezártak prototípusa.
A lezártak hasznosak lehetnek pl. callback függvények használatakor. Egy jó példa erre gombok készítése egy tipikus GUI könyvtárban, ahol minden gombnak van egy callback függvénye, ami a megnyomásakor hajtandó végre. Minden gombnál valami mást szeretnénk végrehajtani. Egy digitális számológépnek pl. 10 hasonló gombra van szüksége, számjegyenként egyre. Ezeket létrehozhatjuk egy hasonló függvénnyel:
A példában feltételezzük, hogy a Button
egy könyvtári függvény, amely új
gombokat készít, a label
a gomb felirata és az action
a callback
függvény. A callback függvény helyesen működik azután is, hogy a digit
változó
hatóköre rég megszűnt.
A lezártak egy teljesen más kontextusban is hasznosak lehetnek. A Luában gyakran előfordul,
hogy újradefiniálunk függvényeket (amit megtehetünk, hiszen sima változókban tároljuk őket).
Sokszor azonban egy függvény újradefiniálásakor szükségünk van a régire az új implementációban.
Például tegyük fel, hogy a math
könyvtár sin
függvényét akarjuk
átszabni úgy, hogy fokok helyett radiánban számoljon. Ez akkor a legegyszerűbb, ha az új függvény
átváltja az argumentumát fokokba, majd meghívja a régi implementációt. Valahogy így:
Ezzel a megoldással a régi verziót a lezárt egy változójában tároljuk, így egyedül az új függvény férhet hozzá.
A Lua a funkcionális nyelvekhez hasonlóan támogatja a rekurzív hívások optimalizálását. Egész pontosan ez a lehetőség speciális függvényhívásokra, az ún. tail call-okra (véghívás, ld.: Lua 5.1 Reference Manual fordítás) vonatkozik. Így nevezzük azt az esetet, amikor egy függvény törzsében az utolsó utasítás egy függvényhívás eredményével való visszatérés. Általános alakban:
Fontos, hogy ha az f
függvénynek még bármit végre kell hajtania a g
után, akkor nem beszélhetünk tail call-ról. A következő példák egyike sem tekinthető annak:
Valódi tail call esetén az f
függvénynek a g
hívása után
már semmi dolga, így a végrehajtási veremben lefoglalt tárterülete felszabadítható.
Ezt biztosítja a Luában proper tail call-nak (tökéletes véghívás) hívott
optimalizációs technika (máshol tail call optimalization vagy tail call
elimination).
A technika igazi haszna rekurzív függvényeknél mutatkozik meg. Rekurzív függvényhívásoknál hagyományos esetben minden egyes hívás saját területet foglal le a veremben, ami így könnyen betelhet. A tail call-t tartalmazó rekurzív függvények esetén viszont minden egyes rekurzív hívás a hívója tárterületét használhatja, így gyakorlatilag végtelen mélységű rekurzió futtatása válik lehetségessé.
A proper tail call-ok lehetővé teszik általában iteratívan megvalósított algoritmusok elegáns, rekurzív implementációját. Lua kód írásakor ezért előnyben szokták részesíteni a rekurzív megoldásokat.
f(1, 2)
függvényhívás után a
értéke 1, b értéke 2, c értéke pedig nil lesz. A három
pont tehát tetszőleges paraméterlistát jelenthet, amivel vissza is lehet térni: