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
8. Odkazy na Internetu
- Programming in Lua (first edition)
http://www.lua.org/pil/index.html - Scope (programming)
http://en.wikipedia.org/wiki/Scope_(programming) - Closure – 3
http://animestudioluascripting.blogspot.com/2007/02/closure-3.html - Anonymous function
http://en.wikipedia.org/wiki/Anonymous_function - Perl (50) – Uzávěry a iterátory
http://www.linuxsoft.cz/article.php?id_article=1397 - Lua home page
http://www.lua.org/ - Lua: vestavitelný minimalista
/clanky/lua-vestavitelny-minimalista/ - Lua
http://www.linuxexpres.cz/praxe/lua - CZ Wikipedia: Lua
http://cs.wikipedia.org/wiki/Lua - EN Wikipedia: Lua (programming language)
http://en.wikipedia.org/wiki/Lua_(programming_language) - The Lua Programming Language
http://www.tiobe.com/index.php/paperinfo/tpci/Lua.html - Lua Programming Gems
http://www.lua.org/gems/ - LuaForge
http://luaforge.net/ - Forge project tree
http://luaforge.net/softwaremap/trove_list.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).