A Lua programozási nyelv

Típusok, típuskonstrukciók

A Lua egy dinamikusan típusos nyelv. Ez azt jelenti, hogy a változóknak nincs típusa, viszont az értékeknek van (minden érték hordozza a típusát). A nyelv alapból referencia szerint végez egyenlõség vizsgálatot, kivételt ezalól csak a nil, boolean, string és number típusok képeznek.

A Lua beépített típusai

A nyelvben minden értékhez tartoznak ún. tag-ek. Ennek segítségével metódusokat definiálhatunk az egyes típusú értékekhez. Ez ad lehetőséget arra, hogy meg tudjuk különböztetni, hogy Lua vagy C típusú értéket tárol egy adott function típusú változó.

Saját típusok konstrukciójára nincs lehetőség, viszont érdekes lehetőségek rejlenek a táblákban. Ld.: Objektum-orientált programozás.

Karakter kódolás

A stringek Luában tetszőleges, minimum 8 bites értékek sorozatai. Közvetlenül a C fordító char típusára képeződnek le (ami lehet 8 bitesnél hosszabb is, ezért a minimum). A nyelv nem foglal le magának konkrét karakter értékeket, így a \0-t sem. Ennek köszönhetően tetszőleges Unicode kódolású szöveget tárolhatunk Lua stringekben.

Az input és output az stdio C könyvtárat használja, amely szöveges módban nem garantálja tetszőleges byte szekvenciák megfelelő kezelését, ráadásul bizonyos byte sorozatok másokra konvertálódnak, pl. a platformspecifikus sorvége jelölés miatt. A fájlokat ezért érdemes mindig bináris módban megnyitni.

Amíg az Unicode szövegeket csak a kódolást támogató külső könyvtáraknak továbbíttjuk, nem kell problémáktól tartanunk. Az egyenlőségvizsgálat vagy a standard könyvtár nemtriviális string műveleteinek használata (pl. mintaillesztés) viszont ilyen stringek esetén nem ajánlott.

Típuskényszerítés

A Lua automatikus futási idejű konverziót hajt végre számok és stringek között. Minden aritmetikai művelet a string típusú paraméteréből megpróbál számot készíteni a szokásos szabályok szerint, ahol pedig stringre lenne szükség, ott a számok alakulnak át valamilyen értelmes formájú stringgé. A számok megjelenési formáját meg is lehet adni a standard könyvtár segítségével.

Összetett típusok

A Lua egyetlen összetett típusa és egyúttal típuskonstrukciós eszköze a table típus.

A táblák segítségével szimulálhatjuk a hagyományos tömb és a rekordszerkezet típusait, hiszen az asszociatív tömb olyan leképezést ír le, amely tetszőleges (nem nil) típusú értékhez egy másik (szintén tetszőleges típusú) elemet rendel.

A fenti összetett típusok típusműveleteit implementálhatjuk függvények segítségével, melyeket a táblában tárolhatunk.

Táblák reprezentációja

A táblák működésének egy másik érdekes jellemzője csak a Lua 5.0 óta szerepel a nyelvben. A 4.0-s verzióval bezárólag a táblák szigorúan hash táblaként voltak implementálva: minden párt explicit tároltak. Az újabb verziókban optimalizálási célokból az egész kulcsú párok kulcsa nem tárolódik, az értékek pedig egy valódi tömbbe kerülnek. Pontosabban, a táblák hibrid adatszerkezetek: tartalmaznak egy hash tábla részt és egy tömb részt. Mivel a felosztás alacsony szinten történik, a tábla mezők hozzáférése egységes, még a virtuális gép számára sincs különbség a kétféleképpen tárolt értékek közt.

A táblák automatikusan és dinamikusan igazítják a részeiket a tartalmukhoz: a tömb rész megpróbálja az 1-től valamely n határig terjedő egész indexek értékeit tárolni. A nem egész indexek, illetve az intervallumon kívül eső egész indexek a hast tábla részbe kerülnek. Ha egy tábla mérete nő (új elemet teszünk bele), a Lua újraszámolja a hash tábla és a tömb méretét. A tömb rész mérete az a legnagyobb n, amire az 1 és n közti indexek legalább fele tartalmaz értéket (hogy ne pazaroljuk a helyet) és legalább egy felhasznált index van n/2 + 1 és n közt (hogy ne legyenek feleslegesen dupla méretű tömbjeink). A tömb és a hash tábla is lehet végül üres. Az új méretek kiszámítása után a Lua létrehozza az új részeket, és újra beszúrja az elemeket a megfelelő helyre.

Egy példa:
Legyen a egy üres tábla, amelynek mind a tömb, mind a hash tábla részének mérete 0. Az a[1] = v1 utasítás végrehajtásához a tábla méretét növelni kell. A Lua az n = 1 -et választja a tömb új méreteként, egyetlen elemmel, a hash tábla rész pedig üres marad. Ha a következő utasítás a[5] = v2, a méretek újraszámításakor a tömb nem nőhet 5 hosszúra, hiszen 1 és 5 közt csak két kihasznált index lenne. A tömb mérete így 1 marad, a hash tábla pedig szintén 1-re nő, és az 5-ös kulcshoz a v2 értékét tárolja. Ha ezután pl. az a[2] = v3 utasítás következik, a méretek kiszámítása után mindhárom érték az 5 hosszú tömbbe kerül, a hash tábla pedig megint üres lesz.

A hibrid sémának két előnye van. Egyrészt az egész kulcsú értékekhez való hozzáférés gyorsabb, mivel nincs szükség hashelésre. Másrészt, ami még fontosabb, a tömb rész nagyjából fele akkora memóriát foglal, mintha ugyanezt a hash táblában tárolnánk, mivel a kulcsok csak implicit léteznek. Következésképpen egy megfelelően sűrűn indexelt tábla tömbként használva tömbökre jellemző teljesítményt nyújt. Mivel a hash tábla rész üres, se sebességben se memóriaigényben nem befolyásolja negatívan a tömböt. Ugyanakkor, ha asszociatív tömbként használjuk a táblát, a tömbrész jó eséllyel üres, így nem foglal plusz memóriát. Ezek a memória megtakarítások fontosak, hiszen Luában gyakran használunk sok kis táblát, például objektumok implementációjára.

Változók, láthatóság

Változók használata

Az egyszerű változóhivatkozások a legtöbb nyelvhez hasonlóan a változó nevének leírásával történnek. Táblák elemeinek elérésére a tömböknél elterjedt szintaxist lehet használni:

név[kulcs]

Gyakran használt eset, amikor string típusú a kulcs - például rekordokat lehet így készíteni. Ezt a felhasználást segítendő, a következő két sor ekvivalens:

név["mező"] név.mező

Változók láthatósága

A Lua háromféle változót ismer: globális és lokális változót, valamint táblamezőt. A függvények formális paraméterei például speciális lokális változók. Lokális változók deklarációja a nevük elé írt local kulcsszóval történik.

A nyelv lexikális láthatóságot használ. Egy változó hatóköre a deklarációt követő első utasításnál kezdődik, és az azt tartalmazó legszűkebb blokk végéig tart. Példa a láthatóságra:

x = 10 -- globális változó do -- új blokk local x = x -- új `x', értéke 10 print(x) --> 10 x = x+1 do -- újabb blokk local x = x+1 -- újabb `x' print(x) --> 12 end print(x) --> 11 end print(x) --> 10 (a gobális 'x')

Ha egy változót nem explicit módon lokálisként deklarálunk, akkor globálisnak számít (minden globális változó "létezik"). A lokális változókhoz a hatókörükön belül definiált függvények szabadon hozzájuk férhetnek. Az első értékadás előtt egy változó értéke mindig nil.

Minden globális változó hagyományos Lua táblákban, ún. környezetekben tárolódik. A változó nevéhez mint kulcshoz van hozzárendelve annak értéke. A Luába exportált C függvények mind egy közös környezeten osztoznak. Minden Luában írt függvénynek saját referenciája van egy környezetre, így a függvényen belül minden globális változó erre a táblára hivatkozik. Egy függvény létrehozásakor örökli a létrehozójának környezetét. A környezetek lekérdezésére és megváltozatására a getfenv és setfenv függvények állnak rendelkezésre.

Kifejezések, operátorok

Atomi kifejezések

A nyelvben a következő elemi kifejezésekből lehet operátorok segítségével kifejezéseket építeni:

Alapkifejezésnek számít még a zárójelezés, amely a szokásos jelentése mellett azt is megteszi, hogy ha a belső kifejezés több értéket ad eredményül, akkor csak az elsőt hagyja meg, a többit eldobja.

A változóhivatkozások leírása a Változók, a függvénydefiníciók az Alprogramok pontban vannak leírva.

Függvényhívás

Egy függvényt Luában is az általánosan elterjedt szintaxissal hívhatunk meg:

függvény(par1, par2, par3)

Paraméter nélküli függvény meghívásakor ki kell tenni az üres zárójelpárt. Ezen kívül néhány rövidítést lehet használni egy paraméteres függvényeknél:

név "..."
név '...'
név [[...]]
név {...}

Az első három sor string literál, az utolsó táblakonstrukció átadását teszi lehetővé egyetlen paraméterként; ugyanazt jelentik, mintha az az egy paraméter zárójelbe lenne téve.

Az utolsó rövidítés az objektum orientált programozást támogatja. Objektumok példánymetódusainak hívásakor a metódus kap egy "rejtett" paramétert, a this hivatkozást (itt self). Luában ezt az implicit paramétert ki kellene írni, de erre is bevezettek egy rövidítést, így az alábbi két sor ugyanazt jelenti:

objektum.függvény(objektum, paraméterek)
objektum:függvény(paraméterek)

Táblakonstrukció

A táblát asszociatív tömb formájában implementáljuk. Az asszociatív tömb indexmezeje nem csak szám, hanem bármilyen típus lehet, ami a Luában fellelhető kivéve a Nil-t. Sőt, a tábláknak nem kell fix méretet megadni, akármekkora lehet - illetve amekkorát a memória megenged. A tábla a fő - valójában az egyetlen - adatszerkezet a Lua nyelvben. Táblákkal reprezentáljuk a "hagyományos" tömböket, szimbolikus táblákat, halmazokat, sorokat, rekordokat és más adatszerkezeteket. Gyakorlatilag a csomagkezelés is táblákkal történik. Mikor azt mondjuk, hogy io.read, ezzel azt szeretnénk elérni, hogy használjuk a io csomag read metódusát. De a háttérben az io tábla read mezőjét - indexét - hívjuk meg.

A legegyszerűbb táblakonstrukció az alábbi:

x = {}

Ez létrehoz egy x nevű táblát, inicializálás nélkül, így minden kulcshoz tartozó eleme nil lesz.

Táblákat létre lehet hozni úgy, hogy egyesével minden kulcshoz értékadással hozzárendeljük a megfelelő értéket. Ennek alternatívájaként olyan kifejezéseket is lehet írni, amelyek táblát adnak eredményül - mintha "tábla literált" írnánk.

A táblakonstrukciós kifejezések kapcsos zárójelek közé zárt mezőfelsorolások. A mezők kulcs-érték párjait vesszővel vagy pontosvesszővel elválasztva kell felsorolni (a két elválasztójelet akár felváltva is használhatjuk), a párokat pedig többféleképpen is meg lehet adni:

[kulcs] = érték ["string"] = érték string = érték érték

Az első a legáltalánosabb alak, a szögletes zárójelek közé bármilyen típusú kifejezést be lehet írni. A második és a harmadik pár megegyezik, ez a szokásos rövidítés a string típusú kulcsokra a rekordok leírásának támogatására. Az utolsó esetben nincs megadva kulcs; ez a táblák leírását kényelmesebbé tevő lehetőség. Az egész konstrukció kap egy 1-től induló számlálót, és minden kulcs nélküli érték ennek a számlálónak az értékét kapja kulcsként, valamint minden ilyen elem elhelyezése után növekszik eggyel a számláló. A másik két típusba tartozó kulcs-érték párok nem számítanak bele ebbe a számolásba.

Ha az utolsó, kulcs nélküli értéket egy függvényhívás szolgáltatja, és ez a függvény több értéket is visszaad, az megint speciális jelentéssel bír: ekkor az összes visszaadott érték bekerül egymás után a táblába. Ezt zárójelezéssel lehet meggátolni, amely csak az első visszaadott értéket engedi betenni a táblába.

A lista végén álló elválasztójelet a Lua figyelmen kívül hagyja. Ezt a kényelmi funkciót a géppel generált kód támogatására vezették be.

Íme egy kimerítő példa:

-- a két blokk ekvivalens a = {[f(1)] = g; "x", "y"; x = 1, f(x), [30] = 23; 45,} do local temp = {} temp[f(1)] = g temp[1] = "x" -- 1st exp temp[2] = "y" -- 2nd exp temp.x = 1 -- temp["x"] = 1 temp[3] = f(x) -- 3rd exp temp[30] = 23 temp[4] = 45 -- 4th exp a = temp end

Mint említettük, a rekordokat is táblákkal valósítjuk meg:

-- a két sor ekvivalens a = {x = 0, y = 0} a = {}; a.x = 0; a.y = 0

Táblák segítségével listák konstruálása is egyszerű, például:

list = nil for line in io.lines() do list = {next = list, value = line} end

Operátorok

A nyelvben a következő operátorok használhatóak, precedencia szerint csökkenő sorrendben:

Operátor Jelentés Megjegyzés
^ hatványozás jobbasszociatív
-x, not, # aritmetikai, logikai negáció
*, / szorzás, osztás
+, - összeadás, kivonás
.. string konkatenáció jobbasszociatív
==, ~=, <, >, <=, >= relációs műveletek
and logikai és rövidzáras szemantika szerint működnek
or logikai vagy

Az egyenlőség vizsgálata nil, a logikai értékek, a számok és a stringek esetén érték szerint történik. Minden más típust referencián keresztül lehet elérni, és az egyenlőségvizsgálat ennek a referenciának az egyenlőségét vizsgálja csak. Ha a két operandus típusa nem egyezik meg, akkor hamisat ad eredményül; még a számok és stringek közötti automatikus konverzió sem működik itt. Így tehát a "0"==0 kifejezés értéke false, ugyanígy a t[0] és t["0"] a tábla különböző elemeit jelentik.

A ~= operátor értéke az egyenlőségvizsgálat (==) negáltja.

A Lua nyelv biztosít egy hossz prefix operátort is, amit #-tel jelölünk. Egy string hossza a string bájtjainak száma (#„helló” == 6).

Táblának csak akkor definiáljuk a hosszát, ha az egy sorozat, vagyis ha az indexei az {1..n} halmaz elemei. Ebben az esetben a tábla hossza n, különben nem definiált.

Logikai operátorok szemantikája

A Lua logikai operátorai: and, or, not. Ezek az operátorok a false és nil értékeket tekintik hamisnak és bármi mást (beleértve a nullát és az üres stringet) igaznak. Ennek főleg történelmi okai vannak, a boolean típus ugyanis elég későn jelent meg a nyelvben.

A not mindig false vagy true értéket ad vissza.

Az and az első argumentumát adja vissza, ha az false vagy nil, egyébként a másodikat.

Az or az első argumentumát adja vissza, ha az nem false vagy nil, egyébként a másodikat.

Példák:

10 or error() -> 10 nil or "a" -> "a" nil and 10 -> nil false and error() -> false false and nil -> false false or nil -> nil 10 and 20 -> 20
Érdekességek logikai operátorokkal

A logikai operátorokkal lehetőségünk van függvényekben default argumentumok használatára. Erre az or operátor szemantikája és a függvényparaméterek szabad kezelése ad lehetőséget, ahogy az alábbi példán láthatjuk:

function foo(n) local iterations = n or 10 blokk end

A Luában bármely függvényt meghívhatjuk (többek közt) úgy, hogy nem minden argumentumot adunk meg. Ha egy, az argumentumlistában szereplő paramétert nem tüntettünk fel a híváskor, értéke a függvény törzsében nil lesz. Így az or, első operandusaként az argumentumot használva, az ő értékét adja vissza ha az argumentumot megadtuk. Ellenkező esetben a második operandust kapjuk vissza, ami így default argumentumértékként viselkedik.

Óvatosnak kell lennünk azonban boolean típusú argumentumok esetén. Az alábbi példa is mutatja, hogy ilyenkor nem nem feltétlen azt a viselkedést kapjuk, amit a valódi default argumentum értékektől elvárnánk:

function foo(flag) local defibrillate = flag or true if defibrillate then ... end end

Hasonló módon lehetséges a feltételes értékadás szimulációja logikai operátorokkal. Erre megint csak az operátorok sajátos szemantikája adja az alapot:

x = (x < n) and (x + 1) or x

A fenti utasítás x-et növeli, ha az kisebb, mint n, különben önmagát kapja értékül. Mivel az and operátor precedenciája nagyobb, először azt értékeljük ki. Az operátor működéséből adódóan x + 1-gyet ad vissza, ha a feltétel teljesül, különben false-t. Ezután az or, igaz feltétel esetén az x + 1-et adja vissza, ha a feltétel igaz volt, hiszen az összeg nem false vagy nil. Ha a feltétel hamis volt (így az első operandus is az), a mádosik operandussal, vagyis x-szel tér vissza.

A fenti formula használata egy esetben nagyon kellemetlen eredményeket hozhat: ha az and után false vagy nil érték következik, akkor mindig a második értéket kapjuk vissza, a feltétel igazságértékétől függetlenül, hiszen a konjunkció eredménye hamis lesz. Így ahogy a default argumentumértékek, a feltételes értékadás is csak részleges megoldásnak tekinthető, amelyet körültekintően szabad csak használnunk.

Szemétgyűjtő

A Lua automatikus memóriakezelést végez, azaz a programozónak nem kell foglalkoznia a memóriafoglalással és a memória felszabadításával akkor, mikor már az objektumaira nincs tovább szüksége. A memória felszabadítását a szemétgyűjtő - Garbage collector - végzi. Ez a mechanizmus időről időre begyűjt minden "halott" objektumot, azaz olyan objetumot, amelyhez már nincs hozzáférése a programnak. Ez minden Lua objektumra vonatkozik: táblák, userdata, sztringek, függvények, szálak stb.

A szemétgyűjtés periodikusan történik. Ehhez a Lua két számot használ fel. Az egyik azt mutatja, hogy mennyi dinamikus memóriát foglalt le a program. A másik pedig a határértéket. Amikor az első szám átlépi a másodikat, azaz a lefoglalt dinamikus memóra több lesz a határértéknél, akkor a Lua elindítja a szemétgyűjtőt és felszabadítja a már "halott" objektumok által lefoglalt memóriát. Ezután az első számértéket átírja az új értékre.

Lehetősége van ezen számok kezelésére a programozónak. Azaz lekérdezhetjünk mindkét értéket illetve a határértéket meg is változtathatjuk. Ha a határértéket nullára állítjuk, akkor a szemétgyűjtés folyamatosan menni fog, ami viszont a program hatékonyságát jelentősen leronthatja. A két értéket a gcinfo() fügvénnyel kérdezhetjük le. A collectgarbage([limit]) fügvénnyel pedig beállíthatjuk a határértéket.

A finalizer segítségével nemcsak a Lua szemétgyűjtőjét manipulálhatjuk, hanem az erőforrásokat is (például fájlok lezárásakor, adatbázis vagy hálózati kapcsolatok kezelésekor). Azokat a userdata-kat, amelyek már felszabadultak, de használtuk a __gc mezőt a metatáblájukban, a Lua nem gyűjti be közvetlenül, hanem egy verembe kerülnek. Miután lezajlott a szemétgyűjtés, egy ezzel a kóddal megyegyező függvény fut le:

function gc_event (udata) local h = metatable(udata).__gc if h then h(udata) end end

A végén a finalizer fordított sorrendben kiveszi a veremből az ott letárolt objektumokat.

További információk a szemétgyűjtésről a 13. fejezetben olvashatók.