A Lua programozási nyelv

Alprogramok, modulok

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.

Függvények megadása

Az alprogramok function típusú értékek a nyelvben. Egy ilyen értéket a következő kifejezéssel lehet létrehozni:

function() blokk end

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:

fn = function() blokk end function fn() blokk end

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:

obj.met = function(self, ...) blokk end function obj:met(...) blokk end

A metódus tehát egy olyan függvény, aminek az első paraméterét a selfnek hívják. A kettőspontos hívási technikával összekötve ez tényleg olyan, mintha objektumokhoz definiálnánk metódusokat.

Lokális függvények

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:

local f = function (...) ... end

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:

local function f (...) ... end

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:

local fact = function (n) if n == 0 then return 1 else return n*fact(n-1) -- a fact itt még nem az általunk definiált érték end end

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:

local function fact (n) if n == 0 then return 1 else return n*fact(n-1) -- a lokális fact létezik, erről a fordító gondoskodik end end

Lezártak

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:

function newCounter () local i = 0 return function () -- anonim függvény i = i + 1 -- lokális változó return i end end c1 = newCounter() print(c1()) --> 1 print(c1()) --> 2

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:

c2 = newCounter() print(c2()) --> 1 print(c1()) --> 3 print(c2()) --> 2

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:

function digitButton (digit) return Button{ label = digit, action = function() add_to_display(digit) end } end

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:

do local oldSin = math.sin local k = math.pi/180 math.sin = function (x) return oldSin(x*k) end end

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á.

Rekurzív hívások optimalizációja

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:

function f(x) ... return g(x) end

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:

g(x); return -- a return előtt el kell vetni g esetleges eredményeit return g(x) + 1 -- el kell végezni az összeadást return x or g(x) -- igazodni kell az 1 visszatérési értékhez return (g(x)) -- igazodni kell az 1 visszatérési értékhez

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.

Változó paraméterszámú függvények

Egy függvénynek Luában nem kötelező felsorolni a formális paraméterlistáját. Ha nem tudjuk előre, hogy hány argumentummal szeretnénk a függvényt meghívni, akkor a
function f(...) a, b, c = ... end
alakot használhatjuk. Az 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:
function f(...) -- függvény törzse return ... end
Azt is megtehetjük, hogy az első néhány argumentumnak nevet adunk, és a maradék tetszőleges lehet:
function f(a, b, ...) c, d = ... end