Hlavní navigace

Objektově orientované programování v Lua II

5. 5. 2009
Doba čtení: 15 minut

Sdílet

V deváté části seriálu o programovacím jazyce Lua si ukážeme, jakým způsobem je možné vytvořit objektový systém s využitím metatabulek, událostí a metametod navázaných na události. Pomocí metatabulek lze zajistit jak volání metod objektu, tak i přetížení operátorů, vytvoření getterů a setterů apod.

Obsah

1. Objektově orientované programování v Lua II
2. Metatabulky a metametody
3. Volání metod objektu s využitím události __index
4. První demonstrační příklad: objekt představující komplexní číslo
5. Přetížení operátorů s využitím metametod
6. Druhý demonstrační příklad: operátory +, -, * a / nad komplexními čísly
7. Třetí demonstrační příklad: operátory nad komplexními čísly vracející komplexní číslo
8. Čtvrtý demonstrační příklad: skrytí metatabulky do uzávěru
9. Obsah další části seriálu

1. Objektově orientované programování v Lua II

V předchozí části seriálu o programovacím jazyce Lua jsme si ukázali dva základní způsoby vytváření objektů. První způsob je založen na uzávěrech (closures), při jejichž použití jsou atributy i metody objektu „zabaleny“ do uzávěru. Druhý způsob je založený na asociativních polích, do kterých lze kromě atributů objektů uložit i funkce představující metody daného objektu. Taktéž jsme si řekli, že při deklaraci i volání metod lze parametr self (což je většinou asociativní pole s atributy objektu) předávat buď explicitně nebo automaticky, což je přístup použitý například i v „klasických“ programovacích jazycích typu Java či C++ (parametr self je zde přejmenovaný na this, jeho význam je však stejný).

Dnes si ukážeme, jakým způsobem lze s využitím takzvaných metatabulek a metametod vytvořit objekty, jejichž metody jsou volané stylem objekt.název_me­tody(). Taktéž si popíšeme způsob přetěžování operátorů, který je v jazyku Lua velmi jednoduchý a přitom i poněkud obecnější, než je tomu například v již výše zmíněném jazyku C++ (podstatný rozdíl spočívá v tom, že Lua je dynamický jazyk, takže některé programové konstrukce lze vytvářet či měnit až za běhu programu, zatímco C++ se spoléhá na silnou typovou kontrolu v době překladu). Nejprve si připomeňme způsob vytvoření objektu popsaný v předchozí části tohoto seriálu. Vytvořený objekt se svými vlastnostmi podobá spíše struktuře (záznamu) či třídě se statickými metodami (funkcemi), což odpovídá tomu, že Lua nebyl primárně navržen jako objektově orientovaný programovací jazyk:

-- Vytvoreni objektu komplexnich cisel a zpusob volani metod

-- Asociativni pole obsahujici metody
Complex={}

-- Konstruktor (ve skutecnosti jen vhodne
-- pojmenovana funkce) ulozena do asociativniho
-- pole Complex.
function Complex.new(paramReal, paramImag)
    local self={}
    self.real = paramReal
    self.imag = paramImag
    return self
end

-- Metoda print s implicitnim predanim parametru self
-- (opet se jedna o funkci ulozenou do asociativniho
-- pole Complex, zde je ovsem automaticky doplnovan
-- parametr self)
function Complex:print()
    print(self.real.."+i"..self.imag)
end

-- Metoda add s implicitnim predanim parametru self
function Complex:add(paramReal, paramImag)
    self.real = self.real + paramReal
    self.imag = self.imag + paramImag
end

-- vytvoreni dvojice objektu
c = Complex.new(1, 2)
c2 = Complex.new(3, 4)

-- tisk hodnot obou objektu
Complex.print(c)
Complex.print(c2)

-- zmena atributu prvniho objektu
Complex.add(c, 10, 20)

-- tisk hodnot obou objektu
Complex.print(c)
Complex.print(c2)

-- finito 

2. Metatabulky a metametody

Význačným a do jisté míry i unikátním znakem programovacího jazyka Lua je možnost přiřadit prakticky každé hodnotě takzvanou metatabulku (metatable). Přímo z Lua skriptu lze sice přiřadit metatabulku pouze asociativním polím, nikoli ostatním typům hodnot, ovšem z céčkového programu (který volá interpretr Lua) je možné měnit i metatabulku ostatních hodnot pomocí API funkce lua_setmetata­ble(). Metatabulka je běžné asociativní pole, se kterým lze provádět stejné operace, jako s každým jiným asociativním polem v Lue, ovšem význam některých položek uložených v tomto poli má speciální význam při práci s danou hodnotou (povšimněte si, že metatabulka je, stejně jako typ, přiřazena přímo hodnotě a nikoli proměnné, což je význačný rys prakticky všech dynamicky typovaných jazyků). Metatabulku je možné asociativnímu poli přiřadit funkcí setmetatable(ob­jekt, metatabulka), opětovné získání metatabulky se provádí funkcí getmetatable(). V případě, že se při volání setmetatable() místo metatabulky předá konstanta nil, je původní metatabulka odstraněna. Naopak funkce getmetatable() může vrátit konstantu nil v případě, že žádná metatabulka nebyla ještě objektu (asociativnímu poli) přiřazena:

-- Zaklad prace s metatabulkami

-- Asociativni pole predstavujici zaklad objektu
objekt = {}

-- Asociativni pole, ktere bude pouzito jako metatabulka
metatable = {}

-- Ziskani metatabulky (vrati se nil, ktere se posleze vytiskne)
print(getmetatable(objekt))

-- Prirazeni metatabulky k objektu
setmetatable(objekt, metatable)

-- Ziskani metatabulky (vrati se prazdne pole, nikoli nil)
table = getmetatable(objekt)

print(table)

-- Tisk obsahu metatabulky
for key, value in ipairs(table) do
    print(key, value)
end

-- finito 

3. Volání metod objektu s využitím události __index

Do metatabulky jsou ukládány takzvané metametody navázané na události (event). Událost je představována svým jménem, tj. řetězcem, který slouží jako klíč v metatabulce (například „__add“, „__eq“ či „__index“), metametoda je naopak libovolná uživatelem definovaná funkce přiřazená k danému klíči. Pomocí metametod lze určit, jakým způsobem se bude interpret jazyka Lua chovat v případech, kdy nad objektem, ke kterému je metatabulka přiřazena, zavoláme nějaký operátor, například +, *, < či dokonce . (přístup k prvku asociativního pole) nebo [] (stejná operace, ovšem zapisovaná pomocí „indexových“ závorek). Právě poslední dva operátory jsou důležité při konstrukci objektového systému, neboť se předefinováním jejich chování dá zajistit, aby se nějaká metoda volala stylem objekt.název_me­tody() a nikoli v první kapitole ukázaným voláním název_třídy.ná­zev_metody(in­stance_třídy). V následující tabulce jsou vypsány operátory, jejichž chování je možné pomocí metametod změnit, i názvy příslušných metametod (před všechny názvy je přidána dvojice podtržítek, protože právě v této podobě jsou události pojmenovány při jejich ukládání do metatabulky):

Operátor Metametoda
+ __add
__sub
* __mul
/ __div
% __mod
^ __pow
__unm (unární minus)
.. __concat (původně spojení řetězců)
# __len
== __eq
< __lt
<= __le
. __index, __newindex
[] __index, __newindex
volání funkce __call

Povšimněte si, že význam některých základních operátorů (viz první dva díly tohoto seriálu) nelze nastavit. Týká se to například operátoru > nebo ~=. Interpret Lua totiž automaticky převádí výraz typu a~=b na not(a==b) a a<=b na not(b<a); navíc pak předpokládá komutativitu všech relačních operátorů. Počet parametrů, se kterými je volána metametoda, odpovídá aritě operátoru – většina operátorů je binárních, tj. metametoda je zavolána se dvěma parametry, některé operátory jsou však pouze unární, především unární minus a operátor zjištění délky či velikosti objektu (#). Mezi binární operátory jsou počítány i operátory [] a ., protože se při jejich volání předává jak asociativní pole, tak i klíč do tohoto pole. Metametoda přiřazená události __index je zavolána při čtení z asociativního pole, metametoda příslušející k události __newindex se volá v případě zápisu nového prvku (tj. prvku, pro nějž prozatím neexistuje klíč) do asociativního pole.

4. První demonstrační příklad: objekt představující komplexní číslo

V dnešním prvním demonstračním příkladu je ukázán vylepšený způsob konstrukce objektů představujících komplexní čísla. Nejprve je vytvořena dvojice globálních asociativních polí Complex a complex_meta. V prvním poli budou uloženy funkce (tj. ve své podstatě metody objektu), druhé pole představuje metatabulku s jediným záznamem (dvojicí událost:metametoda) __index = Complex, pomocí něhož je zajištěn přístup k metodám pro všechny objekty typu komplexní číslo. Dále jsou v příkladu vytvořeny dvě funkce–metody uložené do asociativního pole Complex. První funkce nese název new (jedná se o konstruktor), druhá funkce se jmenuje print.

Při vytváření obou výše zmíněných funkcí byl použit syntaktický cukr ve formě dvojtečky (namísto tečky), což znamená, že obě funkce mají jeden skrytý parametr self, který je však využitý pouze ve funkci print. Po vytvoření třech objektů typu komplexní číslo je ukázáno, jakým způsobem lze zavolat funkci–metodu print. K dispozici jsou tři možnosti – volání přes „třídu“ Complex, volání přes metatabulku s explicitním naplněním parametru self (většinou se nepoužívá) a konečně volání přes metatabulku s automatickým naplněním parametru self, což je chování prakticky shodné například s Javou či C++:

-- Prvni demonstracni priklad:
-- komplexni cislo reprezentovane objektem

Complex = {}

-- Metatabulka pro objekty predstavujici komplexni cisla
complex_meta = {
    -- Pro "objektovy" zpusob volani metod
    __index = Complex
}

-- Konstruktor pro komplexni cisla
function Complex:new(real, imag)
    -- asociativni pole pro ulozeni atributu
    local value = { real = real, imag = imag }
    return setmetatable(value, complex_meta)
end

-- Metoda print
function Complex:print()
    print(self.real, self.imag)
end

-- Objekty predstavujici komplexni cisla
z1 = Complex:new(1, 2)
z2 = Complex:new(3, 4)
z3 = Complex:new(5, 6)

-- Ruzne zpusoby zavolani metody print()

-- primo pres "tridu" - nepouziva se metatabulka
Complex.print(z1)

-- pres metatabulku, explicitni naplneni parametru self
z2.print(z2)

-- pres metatabulku, vyuziti syntaktickeho cukru
-- pro automaticke naplneni parametru self
z3:print()

-- finito 

5. Přetížení operátorů s využitím metametod

Pomocí metatabulek a metametod je poměrně snadné přetížit některé aritmetické nebo relační operátory a vytvořit tak plnohodnotné nové datové typy. Přetížení (overloading) operátorů v programovacím jazyce Lua se odlišuje od přetížení implementovaného například v C++ tím, že je kdykoli za běhu programu možné chování přetíženého operátoru změnit – přetížený operátor je totiž implementován metametodou uloženou v metatabulce, přičemž obsah metatabulek lze, stejně jako obsah dalších tabulek (asociativních polí), libovolně měnit i za běhu programu (runtime), zatímco v C++ lze chování přetíženého operátoru za běhu změnit pouze pomocí polymorfismu. Samotný způsob přetížení zvoleného operátoru či operátorů je velmi jednoduchý a již jsme si ho vlastně ukázali v předchozím demonstračním příkladu – do metatabulky přiřazené k objektu postačuje přidat příslušnou událost a metametodu, tj. funkci přiřazenou ke zvolené události – viz demonstrační příklad uvedený v následující kapitole.

6. Druhý demonstrační příklad: operátory +, -, * a / nad komplexními čísly

V dnešním druhém demonstračním příkladu je ukázáno, jakým způsobem je možné přetížit aritmetické operátory součtu, rozdílu, součinu a podílu pro nový datový typ představující komplexní čísla. Třída komplexních čísel je uložena v asociativním poli Complex, včetně konstruktoru Complex:new() a metody Complex:print(). Tomuto asociativnímu poli je přiřazena metatabulka nazvaná complex_meta, ve které jsou přetíženy operátory ., [] (nutné pro volání metod stylem objekt.název_me­tody), +, -, * a /. Všechny čtyři přetížené aritmetické operátory očekávají dva parametry, jež jsou typu komplexní číslo, tj. musí se jednat o asociativní pole s dvojicí prvků majících klíče „real“ a „imag“. Na výpisu zdrojového kódu demonstračního příkladu si povšimněte, že jednotlivé metametody jsou při konstrukci metatabulky complex_meta od sebe odděleny čárkou (je zapsána za klíčovým slovem end):

-- Druhy demonstracni priklad:
-- pretizeni operatoru +, -, * a / pro komplexni cisla

Complex = {}

-- Metatabulka pro objekty predstavujici komplexni cisla
complex_meta = {
    -- Pro "objektovy" zpusob volani metod
    __index = Complex,

    -- Metametoda pro operator +
    __add = function(x, y)
        return { real = x.real + y.real, imag = x.imag + y.imag }
    end,

    -- Metametoda pro operator -
    __sub = function(x, y)
        return { real = x.real - y.real, imag = x.imag - y.imag }
    end,

    -- Metametoda pro operator *
    __mul = function(x, y)
        return { real = x.real * y.real - x.imag * y.imag, imag = x.real * y.imag + x.imag * y.real }
    end,

    -- Metametoda pro operator /
    __div = function(x, y)
        local mag = y.real ^ 2 + y.imag ^ 2
        local y_upravene = { real = y.real/mag, imag = - y.imag/mag}
        return __mul(x,y_upravene)
    end
}

-- Konstruktor pro komplexni cisla
function Complex:new(real, imag)
    local value = { real = real, imag = imag }
    return setmetatable(value, complex_meta)
end

-- Metoda print
function Complex:print()
    print(self.real, self.imag)
end

-- Objekty predstavujici komplexni cisla
z1 = Complex:new(4, 3)
z2 = Complex:new(2, 0)

-- Zkouska pretizeni aritmetickych operatoru
x = z1 + z2
print(x.real, x.imag)

y = z1 - z2
print(y.real, y.imag)

z = z1 * z2
print(z.real, z.imag)

w = z1 / z2
print(w.real, w.imag)

-- Vysledek operaci ma sice typ asociativni pole,
-- ale jiz nema prirazenou metatabulku, proto
-- pokus o vyvolani operatoru + vede k chybe
a = x + y

-- finito 

7. Třetí demonstrační příklad: operátory nad komplexními čísly vracející komplexní číslo

Demonstrační program ukázaný v předchozí kapitole měl jednu poměrně zásadní vadu – přetížené operátory sice skutečně vypočítaly na základě svých dvou operandů (=parametrů metametody) nové komplexní číslo reprezentované asociativním polem s dvojicí záznamů s klíči „real“ a „imag“, ovšem k výslednému objektu již nebyla navázána metatabulka a tudíž ani příslušné přetížené operátory. Na dvojici komplexních čísel z1 a z2 vytvořených pomocí konstruktoru Complex:new() tedy bylo možné aplikovat přetížený operátor +, ovšem na výsledek této operace již + nebylo možné zavolat, což v předchozím programu vedlo k chybě při pokusu o spuštění příkazu a = x + y. Nicméně tento nedostatek lze jednoduše odstranit – přetížené operátory mohou místo jednoduchého asociativního pole vrátit asociativní pole s metatabulkou přiřazenou pomocí funkce setmetatable() tak, jak je ukázáno v následujícím výpisu kódu. Funkce setmetatable() vrací asociativní pole s přiřazenou metatabulkou, proto lze celé tělo funkce představující přetížený operátor zkrátit na jediný řádek.

-- Treti demonstracni priklad:
-- pretizeni operatoru +, -, * a / pro komplexni cisla
-- vraceny vysledek je taktez "objektem" typu komplexni cislo
Complex = {}

-- Metatabulka pro objekty predstavujici komplexni cisla
complex_meta = {
    -- Pro "objektovy" zpusob volani metod
    __index = Complex,

    -- Metametoda pro operator +
    __add = function(x, y)
        return setmetatable({real = x.real + y.real, imag = x.imag + y.imag }, complex_meta)
    end,

    -- Metametoda pro operator -
    __sub = function(x, y)
        return setmetatable({ real = x.real - y.real, imag = x.imag - y.imag }, complex_meta)
    end,

    -- Metametoda pro operator *
    __mul = function(x, y)
        local result = { real = x.real * y.real - x.imag * y.imag, imag = x.real * y.imag + x.imag * y.real }
        return setmetatable(result, complex_meta)
    end,

    -- Metametoda pro operator /
    __div = function(x, y)
        local mag = y.real ^ 2 + y.imag ^ 2
        local y_upravene = { real = y.real/mag, imag = - y.imag/mag}
        return __mul(x,y_upravene)
    end
}

-- Konstruktor pro komplexni cisla
function Complex:new( real, imag )
    local value = { real = real, imag = imag }
    return setmetatable(value, complex_meta)
end

-- Metoda print
function Complex:print()
    print(self.real, self.imag)
end

-- Objekty predstavujici komplexni cisla
z1 = Complex:new(4, 3)
z2 = Complex:new(2, 0)

-- Zkouska pretizeni aritmetickych operatoru
x = z1 + z2
x:print()

y = z1 - z2
y:print()

z = z1 * z2
z:print()

w = z1 / z2
w:print()

-- Vysledek operaci nad komplexnimi cisly
-- ma prirazenou metatabulku pro komplexni cisla
Complex.print(x + y)
Complex.print(y - z)
Complex.print(z * w)
Complex.print(w / z)

-- finito 

8. Čtvrtý demonstrační příklad: skrytí metatabulky do uzávěru

Ve čtvrtém příkladu je ukázáno, jakým způsobem je možné „skrýt“ metatabulku do uzávěru. Korektně naplněnou metatabulku totiž potřebujeme mít k dispozici ve chvíli vytváření objektu, tj. uvnitř funkce–konstruktoru Complex:new(). Je tedy možné metatabulku vytvořit jako lokální proměnnou této funkce, nikoli proměnnou globální (podobně i asociativní pole pro ukládání atributů je lokální proměnná). Takto upravený program bude pracovat korektně, ovšem až do chvíle, kdy se pokusíme použít nějaký přetížený operátor nad proměnnými (hodnotami), které vznikly předešlou aplikací přetížených operátorů. Důvod je zřejmý – při vytvoření nové hodnoty například v metametodě představující operátor + se odkazujeme na neexistující metatabulku nazvanou metatable. Chybové hlášení se například vypíše ve chvíli, kdy se zavolá nějaká metoda pracující s objekty typu Complex, samotné zavolání přetíženého operátoru chybu nezpůsobí (za neexistující proměnnou metatable se totiž dosadí hodnota nil, což je v daném kontextu korektní):

lua: lua9_5a.lua:55: attempt to call method `print' (a nil value)
stack traceback:
        lua9_5a.lua:55: in function `test'
        lua9_5a.lua:75: in main chunk
        [C]: ? 

Takto pojmenovaná metatabulka sice skutečně existovala ve chvíli, kdy byly vytvářeny „primární“ objekty typu komplexní číslo pomocí konstruktoru Complex:new() (šlo o lokální proměnnou této metody), ale ve chvíli volání metametody __add(x, y) již takto pojmenovaná metatabulka neexistuje, neboť se nacházíme mimo lexikální rozsah funkce Complex:new() (samotná tabulka může být v operační paměti přítomna, pokud existuje nějaký objekt s typem komplexní číslo, ovšem není navázaná na své původní jméno). Řešení tohoto problému je jednoduché – víme, že objekty, nad nimiž je daný přetížený operátor (například +) volán, korektně naplněnou metatabulku obsahují, tudíž můžeme použít volání getmetatable(x), popř. getmeta­table(y) pro získání potřebné metatabulky. Upravený program, který tuto techniku používá, je vypsán pod odstavcem. Povšimněte si, že jedinou globální proměnnou použitou v tomto příkladu je asociativní pole Complex představující třídu komplexních čísel.

UX DAy - tip 2

-- Ctvrty demonstracni priklad:
-- 1) pretizeni operatoru +, -, * a / pro komplexni cisla
--    vraceny vysledek je taktez "objektem" typu komplexni cislo
-- 2) skryti metatabulky do uzaveru

Complex = {}

-- Konstruktor pro komplexni cisla
function Complex:new( real, imag )
    local value = { real = real, imag = imag }
    local metatable =  {
    -- Pro "objektovy" zpusob volani metod
    __index = Complex,

    -- Metametoda pro operator +
    __add = function(x, y)
        return setmetatable({real = x.real + y.real, imag = x.imag + y.imag }, getmetatable(x))
    end,

    -- Metametoda pro operator -
    __sub = function(x, y)
        return setmetatable({ real = x.real - y.real, imag = x.imag - y.imag }, getmetatable(x))
    end,

    -- Metametoda pro operator *
    __mul = function(x, y)
        local result = { real = x.real * y.real - x.imag * y.imag, imag = x.real * y.imag + x.imag * y.real }
        return setmetatable(result, getmetatable(x))
    end,

    -- Metametoda pro operator /
    __div = function(x, y)
        local mag = y.real ^ 2 + y.imag ^ 2
        local y_upravene = { real = y.real/mag, imag = - y.imag/mag}
        return __mul(x,y_upravene)
    end }
    return setmetatable(value, metatable)
end

-- Metoda print
function Complex:print()
    print(self.real, self.imag)
end

-- Otestujeme funkcnost tridy Complex
function test()
    -- Objekty predstavujici komplexni cisla
    z1 = Complex:new(4, 3)
    z2 = Complex:new(2, 0)

    z1:print()
    z2:print()

    -- Zkouska pretizeni aritmetickych operatoru
    x = z1 + z2
    x:print()

    y = z1 - z2
    y:print()

    z = z1 * z2
    z:print()

    w = z1 / z2
    w:print()

    -- Vysledek operaci nad komplexnimi cisly
    -- ma prirazenou metatabulku pro komplexni cisla
    Complex.print(x + y)
    Complex.print(y - z)
    Complex.print(z * w)
    Complex.print(w / z)
end

-- Spusteni testu
test()

-- finito 

9. Obsah další části seriálu

V následující části seriálu o programovacím jazyce Lua si ukážeme způsob vytváření setterů a getterů známých například z jazyka C# s využitím takzvaného proxy objektu, jehož princip je založen – jako mnohé další jazykové konstrukce – na metametodách navázaných na události __index a __newindex. Dále si vysvětlíme použití takzvaných koprogramů (coroutines), pomocí nichž je možné vytvořit programový kód, jehož části se mohou spouštět paralelně. Právě podpora paralelně běžících částí kódu přímo v syntaxi jazyka může v blízké budoucnosti znamenat malý převrat v použití programovacích jazyků, protože jen jejich malá část z nich podporuje skutečné paralelní programování a z vývoje posledních generací mikroprocesorů je patrné, že dalšího zvýšení jejich výpočetního výkonu bude dosahováno právě využitím paralelizace, a to jak na úrovni bloků v aritmeticko-logických jednotkách, tak i celých procesorových jader (ALU+řadič+vy­rovnávací paměť první úrovně).

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.