Funkce v programovacím jazyku Lua - uzávěry

7. 4. 2009
Doba čtení: 13 minut

Sdílet

V páté části seriálu o programovacím jazyku Lua si ukážeme, jakým způsobem je možné pracovat s takzvanými uzávěry (closures). Pomocí uzávěrů lze ve funkcionálních jazycích vytvořit objektový systém (tyto jazyky tedy nemají speciální syntaxi pro třídy a objekty), konstruují se řídicí struktury apod.

Obsah

1. Funkce předávané jako parametry
2. První demonstrační příklad – funkce předávané jako parametry
3. Uzávěry
4. Lokální proměnné přístupné z uzávěrů
5. Malá odbočka – uzávěry a JavaScript
6. Uzávěry a objektově orientované programování
7. Druhý demonstrační příklad – použití uzávěrů
8. Odkazy na Internetu
9. Obsah dalšího pokračování seriálu

1. Funkce předávané jako parametry

V předchozí části seriálu o programovacím jazyku Lua jsme si ukázali, jakým způsobem se vytváří funkce. Vzhledem k tomu, že funkce je v Lua plnohodnotným datovým typem, je možné vytvořenou funkci přiřadit do proměnné – ostatně samotné pojmenování funkcí function jmeno(parametry) … end je pouze syntaktickým cukrem ekvivalentním k zápisu promenna = function(parametry) … end. Funkce je samozřejmě možné, ostatně jako jakoukoli jinou hodnotu (libovolného typu), předat i jako parametr jiné funkci. Tento postup se používá poměrně často, například při registraci takzvaných handlerů událostí, tj. funkcí volaných ve chvíli, kdy v aplikaci dojde ke specifikované změně stavu (takto zaregistrované funkce se taktéž nazývají callback funkce). Funkce předávané formou parametrů (popř. funkce uložené v asociativním poli) také v některých případech mohou nahradit neohrabané několikanásobné rozvětvení programu typu if … elseif … else … end.

2. První demonstrační příklad – funkce předávané jako parametry

V dnešním prvním demonstračním příkladu je ukázáno, jakým způsobem lze předávat libovolnou funkci jako parametr do jiné funkce. Základ celého programu tvoří funkce nazvaná printTable, která slouží, jak již její název napovídá, k tisku tabulky obsahující hodnoty libovolné matematické funkce (s definičním oborem zahrnujícím i interval 0..90), která je funkci printTable předána jako její jediný parametr. Dále jsou v programu vytvořeny dvě uživatelská funkce nazvané fce (tj. uložené do proměnné pojmenované fce), jejichž vybrané funkční hodnoty jsou pomocí printTable vytištěny. Vypsány jsou i hodnoty vypočtené pomocí standardních knihovních funkcí math.sin a math.cos (tyto funkce jsou do printTable předány přímo bez použití pomocné proměnné). Povšimněte si, že ve funkci printTable se používá výraz y = func(alfa), tj. funkce předávaná v parametru se volá zcela standardním způsobem. Následuje výpis zdrojového kódu druhého demonstračního příkladu:

-- Prvni demonstracni priklad:
-- funkce predavane jako parametry

-- pomocna funkce, ktera vytiskne tabulku hodnot
-- pro uhly lezici mezi 0 az 90 stupni
-- na zaklade predane funkce
function printTable(func)
    -- tisk adresy funkce v adresnim prostoru VM
    print(func)
    -- tisk uhlu mezi 0 az 90 stupni
    -- a hodnot uzivatelske funkce pro tyto uhly
    -- (je nastaven krok po peti stupnich)
    for i=0, 90, 5 do
        -- prevod stupnu na radiany
        local alfa = math.pi * i / 180.0
        -- volani funkce predane jako parametr
        local y = func(alfa)
        print(string.format("%d\t%6.4f", i, y))
    end
end

-- uzivatelsky definovana funkce ulozena
-- do promenne "fce"
fce = function(x)
    return math.sin(x)
end

-- tisk tabulky
print("fce = function(x) return math.sin(x) end")
printTable(fce)

print()

-- muzeme primo pouzit i knihovni funkci
-- se stejnym vyznamem
print("fce = math.sin")
printTable(math.sin)

print()

-- dalsi uzivatelsky definovana funkce ulozena
-- do promenne "fce" (pri prirazeni se prepise
-- puvodni hodnota teto promenne)
fce=function(x)
    return math.cos(x)
end

-- tisk tabulky
print("fce = function(x) return math.cos(x) end")
printTable(fce)

print()

-- muzeme primo pouzit i knihovni funkci
-- se stejnym vyznamem
print("fce = math.cos")
printTable(math.cos)

-- finito 

Výstup získaný po spuštění druhého demonstračního příkladu:

fce = function(x) return math.sin(x) end
function: 003DA4E8
0       0.0000
5       0.0872
10      0.1736
15      0.2588
20      0.3420
25      0.4226
30      0.5000
35      0.5736
40      0.6428
45      0.7071
50      0.7660
55      0.8192
60      0.8660
65      0.9063
70      0.9397
75      0.9659
80      0.9848
85      0.9962
90      1.0000

fce = math.sin
function: 003D8900
0       0.0000
5       0.0872
10      0.1736
15      0.2588
20      0.3420
25      0.4226
30      0.5000
35      0.5736
40      0.6428
45      0.7071
50      0.7660
55      0.8192
60      0.8660
65      0.9063
70      0.9397
75      0.9659
80      0.9848
85      0.9962
90      1.0000

fce = function(x) return math.cos(x) end
function: 003DB928
0       1.0000
5       0.9962
10      0.9848
15      0.9659
20      0.9397
25      0.9063
30      0.8660
35      0.8192
40      0.7660
45      0.7071
50      0.6428
55      0.5736
60      0.5000
65      0.4226
70      0.3420
75      0.2588
80      0.1736
85      0.0872
90      0.0000

fce = math.cos
function: 003D84B8
0       1.0000
5       0.9962
10      0.9848
15      0.9659
20      0.9397
25      0.9063
30      0.8660
35      0.8192
40      0.7660
45      0.7071
50      0.6428
55      0.5736
60      0.5000
65      0.4226
70      0.3420
75      0.2588
80      0.1736
85      0.0872
90      0.0000 

3. Uzávěry

Ve funkcionálních jazycích, mezi něž programovací jazyk Lua některými svými vlastnostmi bezesporu náleží, se poměrně často používají takzvané uzávěry (closures), které byly poprvé navrženy a implementovány ve známém jazyku Scheme. V jazyku Lua se uzávěry konstruují pomocí anonymní (nepojmenované) funkce vytvořené uvnitř jiné funkce, přičemž tyto anonymní funkce mají přístup k lokálním proměnným „své“ vytvářející funkce. Vytvořené anonymní funkce (resp. odkazy na ně) je možné v jazyku Lua vracet volajícímu programu pomocí příkazu return, neboť funkce jsou v tomto jazyku plnohodnotným datovým typem, se kterým lze manipulovat podobně, jako s ostatními datovými typy (čísly, asociativními poli, pravdivostními hodnotami atd). To, že funkce mohou být návratovými hodnotami jiných funkcí, není nic překvapivého, ostatně s využitím ukazatelů (na funkce) se podobného efektu dá docílit i v nízkoúrov­ňovém céčku.

Zajímavá a velmi důležitá je však jiná vlastnost uzávěrů – jelikož jsou uzávěry vytvořeny uvnitř jiné funkce, mají mj. přístup i ke všem lokálním proměnným této funkce. Co se však stane v případě, že se na tyto proměnné budeme skutečně uvnitř uzávěru odkazovat (například budeme chtít modifikovat jejich hodnotu)? Dokonce se můžeme ptát, zde je vůbec následující příklad korektní, tj. zda po svém spuštění nevypíše chybové hlášení při pokusu o přístup k proměnné y:

-- Funkce obsahujici lokalni promennou.
-- Tato funkce vraci anonymni funkci
-- jako svuj vysledek.
function generateClosure()
    -- lokalni promenna, ktera neni
    -- z okolniho programu dostupna
    local y = 1
    -- anonymni funkce vytiskne hodnotu
    -- lokalni promenne funkce "generateClosure"
    -- a potom se pokusi o zmenu jeji hodnoty:
    return function()
        print(y)
        y = y + 1
    end
end

-- ziskame uzaver, tj. instanci anonymni funkce
closure1 = generateClosure()

-- jake hodnoty se vytisknou?
closure1()
closure1()
closure1()
closure1()

-- finito 

4. Lokální proměnné přístupné z uzávěrů

Na první pohled by se mohlo zdát, že lokální proměnné funkcí zaniknou ve chvíli, kdy program opustí tělo této funkce (přesněji – lokální proměnná existuje jen v rámci svého lexikálního kontextu). Ovšem u funkcionálních jazyků si každý vytvořený objekt (včetně funkce, zde speciálně uzávěru) uchovává odkazy na všechny proměnné, které jsou uvnitř objektu použity, nehledě na jejich lexikální kontext. To ovšem znamená, že pokud je uvnitř nějaké funkce nazvané generateClosure (viz předchozí příklad) vytvořena anonymní funkce (uzávěr) přistupující k lokálním proměnným funkce generateClosure a tato anonymní funkce je vrácena příkazem return, jsou všechny odkazované lokální proměnné vytvořené uvnitř generateClosure zachovány minimálně po tu dobu, po kterou existuje vrácený uzávěr. Každé volání uzávěru může s těmito lokálními (a zdánlivě už neexistujícími) proměnnými pracovat, tj. číst i zapisovat do nich hodnoty. To znamená, že program uvedený v předchozí kapitole skutečně funguje – po svém spuštění vypíše posloupnost 1 – 2 – 3 – 4.

Vzhledem k tomuto chování není ve funkcionálních jazycích podporujících uzávěry obecně možné všechny lokální proměnné ukládat na zásobník (jeho rámec je po opuštění funkce zapomenut), ale je nutné využít spíše paměť alokovanou na haldě (heap), pro jejíž uvolňování je použita nějaká forma automatického uvolňování nepoužívané paměti (garbage collectoru). Vzhledem k tomu, že uzávěr pracuje skutečně s odkazem na lokální proměnnou vytvořenou v nadřazené funkci, vypíše následující program posloupnost 42 – 43 – 44 a 45, neboť poslední hodnota proměnné y před opuštěním funkce byla nastavena na 42:

-- Funkce obsahujici lokalni promennou.
-- Tato funkce vraci anonymni funkci
-- jako svuj vysledek.
function generateClosure()
    -- lokalni promenna, ktera neni
    -- z okolniho programu dostupna
    local y = 1
    -- anonymni funkce vytiskne hodnotu
    -- lokalni promenne funkce "generateClosure"
    -- a potom se pokusi o zmenu jeji hodnoty:
    local result = function()
        print(y)
        y = y + 1
    end
    -- po vytvoreni zarodku uzaveru
    -- zmenime hodnotu lokalni promenne
    y = 42
    -- vratime vytvorenou funkci - uzaver
    return result
end

-- ziskame uzaver, tj. instanci anonymni funkce
closure = generateClosure()

-- vytiskne se posloupnost hodnot 42, 43, 44 a 45
closure()
closure()
closure()
closure()

-- finito 

Důležité je, že původní lokální proměnné jsou viditelné pouze uvnitř uzávěru, nejedná se tedy o proměnné globální – ty se ostatně ve funkcionálních jazycích téměř nepoužívají, což je jen dobře (již minule jsme si řekli, že používání globálních proměnných například omezuje paralelizovatelnost programu i jeho testování). Samozřejmě platí, že v případě nového volání funkce generateClosure se vytvoří nové lokální proměnné i nový uzávěr, tj. jednotlivé uzávěry mají své vlastní kopie původních lokálních proměnných (lokální proměnné tedy nejsou statické ve smyslu „statičnosti“ známém například z céčka). Následující program vytiskne po svém spuštění dvě řady čísel 1 – 2 – 3 – 4, protože uzávěr nazvaný closure1 (přesněji řečeno uzávěr uložený do proměnné pojmenované closure1) používá odlišnou kopii lokální proměnné y nadřazené funkce generateClosure než uzávěr nazvaný closure2. Každé volání funkce generateClosure tedy vede k alokaci paměti na haldě; do této paměti je uložena počáteční hodnota lokální proměnné y a vytvořený uzávěr obsahuje odkaz na tuto hodnotu:

-- Funkce obsahujici lokalni promennou.
-- Tato funkce vraci anonymni funkci
-- jako svuj vysledek.
function generateClosure()
    -- lokalni promenna, ktera neni
    -- z okolniho programu dostupna
    local y = 1
    -- anonymni funkce vytiskne hodnotu
    -- lokalni promenne funkce "generateClosure"
    -- a potom se pokusi o zmenu jeji hodnoty:
    return function()
        print(y)
        y = y + 1
    end
end

-- ziskame uzaver, tj. instanci anonymni funkce
closure1 = generateClosure()
closure2 = generateClosure()

-- jake hodnoty se vytisknou?
closure1()
closure1()
closure1()
closure1()
closure2()
closure2()
closure2()
closure2()

-- finito 

5. Malá odbočka – uzávěry a JavaScript

Poznamenejme, že prakticky stejným způsobem se uzávěry vytváří a volají například v JavaScriptu. Tento jazyk má sice mezi vývojáři pověst spíše amatérského nástroje, ve skutečnosti se však jedná o relativně expresivní dynamický jazyk, pro který v současnosti existují výkonné interpretry (původní interpretry použité například ve „čtyřkových“ verzích webových prohlížečů trpěly výkonnostními problémy i vzájemnou nekompatibilitou) a samotný jazyk se již nepoužívá pouze na webových stránkách, ale i jako skriptovací jazyk v různých aplikacích, prostředek pro skriptování VRML či SVG apod. Následující fragment HTML stránky po svém zobrazení v prohlížeči a spuštění vloženého skriptu vypíše do okna prohlížeče dvě řady čísel 1 – 2 – 3 – 4; jedná se tedy o stejný výsledek, jaký vypisuje předchozí program napsaný v programovacím jazyku Lua:

<html>
    <body>
        <script type="text/javascript">
            // Funkce obsahujici lokalni promennou.
            // Tato funkce vraci anonymni funkci
            // jako svuj vysledek.
            function generateClosure()
            {
                // lokalni promenna, ktera neni
                // z okolniho programu dostupna
                var y = 1;
                // anonymni funkce vytiskne hodnotu
                // lokalni promenne funkce "generateClosure"
                // a potom se pokusi o zmenu jeji hodnoty:
                return function()
                {
                    document.write(y + "<br />");
                    y = y + 1;
                };
            }
            // ziskame uzaver, tj. instanci anonymni funkce
            closure1 = generateClosure();
            closure2 = generateClosure();

            // jake hodnoty se vytisknou?
            closure1();
            closure1();
            closure1();
            closure1();
            closure2();
            closure2();
            closure2();
            closure2();
        </script>
    </body>
</html> 

6. Uzávěry a objektově orientované programování

Princip uzávěrů je v mnoha ohledech podobný klasickým objektům známým z OOP (objektově orientovaného programování), ostatně ve výše zmíněném jazyku Scheme bylo vytvořeno mnoho objektových knihoven postavených právě na uzávěrech. Avšak zatímco objekty v OOP jsou primárně (především ze syntaktického hlediska) založeny na datových položkách (atributech), ke kterým jsou přidány metody určené pro manipulaci s těmito daty, jsou u uzávěrů naopak primárním typem funkce, ke kterým jsou připojena data, se kterými funkce pracuje. Není tedy neobvyklé si například vytvořit více uzávěrů, které si, podobně jako metody objektů, mezi sebou sdílejí soukromá data – lokální proměnné funkce, ve kterých jsou uzávěry vytvořeny. To, který koncept se v programu použije, záleží především na vlastnostech použitého programovacího jazyka a osobních preferencích programátora. Lua podporuje oba koncepty (uzávěry i OOP založené na prototypech), podobně jako například programovací jazyk JavaScript, Python, Python či některé dialekty Lispu.

Podpora uzávěrů v některém programovacím jazyce navíc v praxi de facto vyžaduje, jak jsme si již naznačili o několik odstavců výše, existenci automatické správy přidělované paměti (garbage collection), neboť lokální proměnné používané v uzávěrech nelze z výše popsaných důvodů ukládat na zásobník jako ostatní lokální proměnné, ze kterého by byly automaticky odstraňovány při opuštění funkce. Taktéž tyto proměnné nelze uložit do staticky alokovaného paměťového prostoru tak, jak je tomu například u céčkovských globálních proměnných či statických lokálních proměnných (v podstatě se z hlediska překladače taktéž jedná o globální proměnné, ovšem s omezenou viditelností). Z tohoto důvodu se s podporou uzávěrů setkáme spíše ve vysokoúrovňových programovacích jazycích, které nějakou formu automatické správy paměti obsahují (u Scheme se například rozpoznává, které proměnné jsou čistě lokální a které se používají v uzávěrech a podle toho je také provedena jejich alokace – buď na zásobníku nebo na haldě).

7. Demonstrační příklad – použití uzávěrů

V níže vypsaném demonstračním příkladu je ukázáno použití uzávěrů při výpočtech tabulek celočíselných mocnin o zvoleném základu (například mocnin čísla 2). Každý řádek vytištěné tabulky obsahuje dvě hodnoty – exponent a vypočtenou mocninu. Uzávěr tedy bude přistupovat ke dvojici lokálních proměnných, přičemž jedna proměnná představuje vypočtenou mocninu a druhá exponent (ten se průběžně zvyšuje o jedničku). Navíc uzávěr pracuje i s parametrem funkce, pomocí něhož je při jejím volání specifikován základ. Každé volání uzávěru znamená, že se nejdříve vytiskne hodnota vypočtená v průběhu předchozího volání, vypočte se hodnota pro další řádek tabulky (předchozí hodnota se jednoduše vynásobí základem) a nakonec se zvýší hodnota exponentu o jedničku. Vzhledem k tomu, že každý vytvořený uzávěr má k dispozici vlastní lokální proměnné vytvářející funkce (přesněji odkazy na tyto proměnné), je možné současně počítat více tabulek, například pro různé základy (v příkladu není použito, můžete si však sami vyzkoušet sloučit poslední tři programové smyčky). Tento demonstrační příklad byl inspirován Perlovským kódem popsaným v článku Perl (50) – Uzávěry a iterátory:

-- Demonstracni priklad:
-- pouziti uzaveru

-- pomocna funkce vracejici uzaver
function defPosloupnosti(n)
    -- pamatovana hodnota, ktera vsak
    -- neni z okolniho programu dostupna
    local y = 1
    -- pocitadlo volani = exponent
    local index = 0
    -- anonymni funkce vytiskne pamatovanou
    -- hodnotu a nakonec ji vynasobi zvolenou konstantou
    return function()
        print(index, y)
        y = y * n
        index = index + 1
    end
end

print("mocniny cisla 2")
-- ziskani uzaveru
generator = defPosloupnosti(2)

-- postupne se budou tisknout
-- mocniny cisla 2
for i=0, 16 do
    generator()
end

print()

print("mocniny cisla 3")
-- ziskani uzaveru
generator = defPosloupnosti(3)

-- postupne se budou tisknout
-- mocniny cisla 3
for i=0, 16 do
    generator()
end

print()

print("mocniny cisla 10")
-- ziskani uzaveru
generator = defPosloupnosti(10)

-- postupne se budou tisknout
-- mocniny cisla 3
for i=0, 16 do
    generator()
end

-- finito 

Druhý demonstrační příklad po svém spuštění vypíše trojici tabulek – celočíselné mocniny čísla 2, celočíselné mocniny čísla 3 a konečně celočíselné mocniny čísla 10:

mocniny cisla 2
0       1
1       2
2       4
3       8
4       16
5       32
6       64
7       128
8       256
9       512
10      1024
11      2048
12      4096
13      8192
14      16384
15      32768
16      65536

mocniny cisla 3
0       1
1       3
2       9
3       27
4       81
5       243
6       729
7       2187
8       6561
9       19683
10      59049
11      177147
12      531441
13      1594323
14      4782969
15      14348907
16      43046721

mocniny cisla 10
0       1
1       10
2       100
3       1000
4       10000
5       100000
6       1000000
7       10000000
8       100000000
9       1000000000
10      10000000000
11      100000000000
12      1000000000000
13      10000000000000
14      1e+014
15      1e+015
16      1e+016 

ict ve školství 24

8. Odkazy na Internetu

  1. Programming in Lua (first edition)
    http://www.lu­a.org/pil/index­.html
  2. Scope (programming)
    http://en.wiki­pedia.org/wiki/Sco­pe_(programmin­g)
  3. Closure – 3
    http://animes­tudioluascrip­ting.blogspot­.com/2007/02/clo­sure-3.html
  4. Anonymous function
    http://en.wiki­pedia.org/wiki/A­nonymous_functi­on
  5. Perl (50) – Uzávěry a iterátory
    http://www.li­nuxsoft.cz/ar­ticle.php?id_ar­ticle=1397
  6. Lua home page
    http://www.lu­a.org/
  7. Lua: vestavitelný minimalista
    /clanky/lua-vestavitelny-minimalista/
  8. Lua
    http://www.li­nuxexpres.cz/pra­xe/lua
  9. CZ Wikipedia: Lua
    http://cs.wiki­pedia.org/wiki/Lua
  10. EN Wikipedia: Lua (programming language)
    http://en.wiki­pedia.org/wiki/Lu­a_(programmin­g_language)
  11. The Lua Programming Language
    http://www.ti­obe.com/index­.php/paperinfo/tpci/Lu­a.html
  12. Lua Programming Gems
    http://www.lu­a.org/gems/
  13. LuaForge
    http://luafor­ge.net/
  14. Forge project tree
    http://luafor­ge.net/softwa­remap/trove_lis­t.php

9. Obsah dalšího pokračování seriálu

V následujícím pokračování seriálu o programovacím jazyku Lua si ukážeme, jakým způsobem je možné vytvářené funkce ukládat do asociativních polí (hešovacích map), protože tento postup je v Lua základem pro tvorbu objektů (ve smyslu objektově orientovaného programování – OOP). Taktéž si řekneme, jak lze interpretr jazyka Lua používat ve vlastních céčkových či C++ aplikacích a jak mohou skripty napsané v Lua komunikovat s céčkovými funkcemi či používat datové struktury vytvořené v céčkovém programu. Uvidíme, že programové rozhraní mezi prostředím interpretru, ve kterém běží skripty napsané v Lua, a céčkovým programem je velmi jednoduché, ale taktéž dostatečné na to, aby byla spolupráce obou částí aplikace efektivní a do značné míry bezproblémová a současně i bezpečná (samotný skript napsaný v jazyku Lua běží v takzvaném sandboxu a nemusí mít například povolen přímý přístup k souborovému systému či aplikačnímu programovému rozhraní jádra).

Autor článku

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