Funkce v programovacím jazyku Lua

31. 3. 2009
Doba čtení: 13 minut

Sdílet

Ve čtvrté části seriálu o programovacím jazyku Lua si popíšeme, jak je možné definovat nové funkce, předávat jim parametry (včetně možnosti použití proměnného a volitelného počtu parametrů), získávat návratové hodnoty z funkcí a další užitečné postupy, které Lua převzala především z funkcionálních jazyků.

Obsah

1. Definice funkcí
2. Funkce jako datový typ
3. Parametry funkcí a jejich návratové hodnoty
4. Volitelné parametry funkcí
5. Proměnný počet parametrů
6. Využití proměnného počtu parametrů
7. Rekurze, tail rekurze
8. Obsah další části seriálu

1. Definice funkcí

Ve druhé části tohoto seriálu jsme se seznámili se všemi datovými typy, které je možné v programovacím jazyku Lua použít. Jeden z osmi podporovaných datových typů představují i funkce – samotný fakt, že funkce patří mezi datové typy je z programátorského hlediska velmi významný, neboť je možné, jak si ukážeme v navazujících kapitolách i další části seriálu, funkce přiřazovat proměnným, předávat funkce jako parametry jiným funkcím, ukládat funkce do asociativních polí atd. Ukažme si nyní, jakým způsobem se funkce většinou definují a pojmenovávají (jak uvidíme dále, definice pojmenovaných funkcí je pouze „syntaktickým cukrem“ zavedeným do syntaxe proto, aby se Lua snadněji používala programátorům zvyklým na imperativní jazyky). Definice pojmenované funkce začíná klíčovým slovem function, za nímž následuje název funkce následovaný kulatými závorkami, ve kterých mohou být uvedeny parametry funkce. Za těmito závorkami se nachází tělo funkce, což je ze syntaktického hlediska programový blok (viz druhá část seriálu, kapitola 4) ukončený klíčovým slovem end.

Funkce může pomocí příkazu return vracet libovolné množství (návratových) hodnot. V případě, že příkaz return není před koncem těla funkce (či uvnitř programových smyček popř. podmínek) použit, nevrací funkce žádné hodnoty, tj. ze sémantického hlediska se jedná o proceduru. Uvnitř těla funkce je možné, ostatně jako v kterémkoli jiném programovém bloku, vytvářet lokální proměnné, na tomtéž místě lze používat i proměnné globální (i když se to nedoporučuje, protože se tím narušuje obecná představa o funkcích jakožto objektech, které získávají veškeré informace o svém okolí pomocí parametrů a naopak okolí může získat informace z funkce jen pomocí návratové hodnoty či několika hodnot). Následuje příklad, ve kterém jsou vytvořeny dvě funkce nazvané printHello a printWorld. První z těchto funkcí používá ve svém těle lokální proměnnou nazvanou helloStr, druhá funkce se naopak odkazuje na globální proměnnou worldStr. Povšimněte si, že změna globální proměnné ovlivní i chování funkce, tj. vytištěný řetězec:

-- Prvni demonstracni priklad:
-- definice vlastnich funkci

-- funkce obsahujici lokalni promennou
function printHello()
    local helloStr = "Hello"
    print(helloStr)
end

worldStr="World"

-- funkce vyuzivajici globalni promennou
function printWorld()
    -- ke globalni promenne je pripojena
    -- retezcova konstanta (literal)
    print(worldStr .. "!")
end

-- volani obou funkci
printHello()
printWorld()

-- zmena globalni promenne
-- se projevi i ve volane funkci
worldStr = 42
printWorld()

-- finito 

Text vypsaný na standardní výstup po spuštění prvního demonstračního příkladu:

Hello
World!
42! 

2. Funkce jako datový typ

V předchozí kapitole jsme si řekli, že definice funkce ve stylu function jméno() tělo funkce end je ve skutečnosti pouze „syntaktickým cukrem“, který byl do programovacího jazyka Lua přidán především proto, aby se jeho syntaxe přibližovala běžným imperativním jazykům. Ve skutečnosti totiž parametry funkce spolu s jejím tělem přestavují hodnotu typu function, kterou je možné přiřadit do proměnné libovolného jména (dokonce ani k přiřazení nemusí dojít, což je případ nepojmenovaných lokálních funkcí – uzávěrů). „Skutečná“ syntaxe definice funkce tedy neobsahuje její jméno, jak si ostatně ukážeme na druhém demonstračním příkladu vypsaném pod odstavcem. Ve zdrojovém textu tohoto příkladu jsou vytvořeny dvě funkce (tj. ve skutečnosti hodnoty typu function), přičemž první funkce je přiřazena proměnné pojmenované printHello a druhá funkce proměnné printWorld. Výsledkem přiřazení je vytvoření totožných pojmenovaných funkcí, jako v prvním demonstračním příkladu, ovšem pokud se nad příkladem zamyslíme, zjistíme jednu zajímavou věc – ve spuštěném programu je možné, že se hodnota libovolné proměnné může měnit a totéž samozřejmě platí i pro hodnoty typu function. Jinými slovy to znamená, že v jazyku Lua je možné funkce znovu definovat, tj. jejich jméno se sice nemění, ale tělo (tj. implementovaný algoritmus) již ano:

-- Druhy demonstracni priklad:
-- definice vlastnich funkci bez vyuziti
-- "syntaktickeho cukru"

-- funkce obsahujici lokalni promennou
-- (hodnota typu funkce je prirazena
--  promenne pojmenovane "printHello")
printHello = function()
    local helloStr = "Hello"
    print(helloStr)
end

worldStr="World"

-- funkce vyuzivajici globalni promennou
-- (hodnota typu funkce je prirazena
--  promenne pojmenovane "printWorld")
printWorld = function()
    -- ke globalni promenne je pripojena
    -- retezcova konstanta (literal)
    print(worldStr .. "!")
end

-- volani obou funkci
printHello()
printWorld()

-- zmena globalni promenne
-- se projevi i ve volane funkci
worldStr=42
printWorld()

-- finito 

3. Parametry funkcí a jejich návratové hodnoty

Prakticky každá funkce, pokud má provádět užitečnou činnost, musí nějakým způsobem komunikovat se svým okolím. V té nejjednodušší (a nejméně vhodné) podobě lze použít globální proměnné, ke kterým má funkce samozřejmě přístup – výsledný program se však sémanticky podobá spíše BASICovým konstrukcím s podprogramy a nikoli vysokoúrovňovému zápisu algoritmu, nehledě na to, že funkce používající globální proměnné, se například velmi těžko testují či používají při paralelním programování. Mnohem lepší je funkci předávat informace pomocí parametrů, jejichž jména (nikoli typy – ty jsou, jak již víme z předchozích částí seriálu, přiřazeny hodnotám) jsou zapsána uvnitř kulatých závorek. Funkce taktéž může vracet jednu nebo více (!) hodnot pomocí konstrukce return seznam_hodnot. Vzhledem k existenci operátoru vícenásobného přiřazení a možnosti vrátit větší množství parametrů jsou tak funkce v jazyku Lua obecnější, než v některých dalších programovacích jazycích, ve kterých funkce většinou mohou vracet jen jednu hodnotu (i díky této vlastnosti se v Lua nemusí zavádět konstrukce typu volání hodnotou a volání odkazem).

Následuje demonstrační příklad, v němž je vytvořeno a následně použito (zavoláno) několik funkcí. První funkce nazvaná repeatMessage má parametry, ale nevrací žádnou hodnotu, druhá funkce factorial má jeden parametr a vrací buď nil pro parametr, který neodpovídá definičnímu oboru faktoriálu, nebo přirozené číslo vyjadřující n! (samotný faktoriál je vypočten nerekurzivně, jeho rekurzivní zápis je uveden v navazujících kapitolách). Třetí funkce divMod má dva parametry x,y a vrací dvě celočíselné hodnoty – celočíselný podíl x div y a zbytek po dělení x mod y (někteří historici tvrdí, že právě tato matematická „dvojoperace“ byla první početní operací, kterou lidé používali; své využití měla především při rozdělování kořisti). Funkce divMod je následně použita pro vytvoření tabulky celočíselného dělení i zbytku po dělení čísla 10 hodnotami od jedné do desíti. Následuje výpis zdrojového kódu třetího demonstračního příkladu:

-- Treti demonstracni priklad:
-- funkce s parametry, funkce vracejici hodnoty

-- funkce se dvema parametry,
-- ktera nevraci zadnou hodnotu
function repeatMessage(message, count)
    for i = 1, count do
        print(i, message)
    end
end

-- nerekurzivni vypocet faktorialu
-- (funkce s jednim parametrem vracejici taktez jednu hodnotu)
function factorial(n)
    local result = 1
    -- faktorial je definovan pouze pro prirozena cisla a nulu
    if n < 0 then
        return nil
    end
    for i = 1, n do
        result = result * i
    end
    return result
end

-- funkce vracejici dve hodnoty: vysledek celociselneho
-- deleni a zbytek po celociselnem deleni
function divMod(x,y)
    return math.floor(x / y), x % y
end

-- volani funkci
repeatMessage("Hello world!", 10)

print()
print("Factorial")
print("n", "n!")
for n = -5, 10 do
    print(n, factorial(n))
end

print()
print("DivMod")
print("n", "10/n", "zbytek")
for n = 1, 10 do
    x, y = divMod(10, n)
    print(n, x, y)
end

-- finito 

Výše uvedený demonstrační příklad po svém spuštění vypíše na standardní výstup následující text:

1    Hello world!
2       Hello world!
3       Hello world!
4       Hello world!
5       Hello world!
6       Hello world!
7       Hello world!
8       Hello world!
9       Hello world!
10      Hello world!

Factorial
n       n!
-5      nil
-4      nil
-3      nil
-2      nil
-1      nil
0       1
1       1
2       2
3       6
4       24
5       120
6       720
7       5040
8       40320
9       362880
10      3628800

DivMod
n       10/n    zbytek
1       10      0
2       5       0
3       3       1
4       2       2
5       2       0
6       1       4
7       1       3
8       1       2
9       1       1
10      1       0 

4. Volitelné parametry funkcí

Programovací jazyk Lua podporuje, podobně jako některé další programovací jazyky, volitelné parametry u funkcí. Pokud je například funkce nazvaná fce definována se třemi parametry x, y a z, není nutné, aby se při volání této funkce skutečně musely uvést hodnoty všech tří parametrů (příkladem z praxe může být funkce io.open, kterou lze volat buď jen se jménem otevíraného souboru, nebo lze ve druhém parametru uvést i režim otevření: pro čtení, zápis, připojení atd.). Pokud je zmíněná funkce fce volána s hodnotami 1, 2 a 3, jsou samozřejmě parametry vyplněny v tomtéž pořadí: x=1, y=2, z=3. Ovšem tutéž funkci je také možné zavolat se žádnou, jednou či dvěma hodnotami – fce(), fce(42), fce(42,6502). V takovém případě se do zbývajících parametrů, kterým neodpovídá žádná hodnota při volání funkce, dosadí nil. Tato vlastnost programovacího jazyka Lua je předvedena v následujícím demonstračním příkladu, ve kterém funkce fce po svém zavolání vypíše hodnoty svých parametrů:

-- Ctvrty demonstracni priklad:
-- volitelne parametry funkci

function fce(x, y, z)
    print(x, y, z)
end

-- volani funkce fce s ruznym poctem parametru
fce(1, 2, 3)
fce(1, 2, 3, 4) -- posledni hodnota se nevyuzije
fce()
fce(42)
fce(42, 6502)
fce(nil, 6502)
-- lze pouzit i retezce popr. hodnoty dalsich typu typu
fce("Hello", "world", "!")
fce("Hello".." world", "!" )
fce("Hello".." world".."!" )

-- finito 

Program po svém spuštění vypíše následující tabulku:

1               2               3
1               2               3
nil             nil             nil
42              nil             nil
42              6502            nil
nil             6502            nil
Hello           world           !
Hello world     !               nil
Hello world!    nil             nil 

5. Proměnný počet parametrů

Programovací jazyk Lua kromě výše popsaných volitelných parametrů (ty jsou explicitně vyjmenovány v hlavičce funkce) taktéž podporuje i použití proměnného počtu parametrů – v tomto případě jsou v definici funkce uvedeny pojmenované parametry (ty lze uvnitř funkce používat stejným způsobem jako lokální proměnné), za nimiž může následovat libovolný počet parametrů nepojmenovaných, jejichž hodnoty jsou přístupné přes speciální výraz zapisovaný jako tři tečky – (v tomto kontextu se můžeme také setkat s termínem variadické funkce nebo variable number of arguments – varargs). Například funkci g s definicí g(x, y, …) je možné předat nula až n parametrů, přičemž hodnota prvního parametru je ve funkci přístupná přes identifikátor x, hodnota druhého parametru přes identifikátor y a hodnota všech dalších parametrů je přístupná přes výraz  – uvnitř těla funkce lze použít buď konstrukci typu a,b,c=…, která proměnným a, b a c přiřadí první tři nepojmenované parametry (pokud těchto parametrů není dostatečný počet, dosadí se do proměnných hodnota nil) nebo lze seznam hodnot převést na (asociativní) pole pomocí konstrukce {…}, což si ukážeme v následujícím demonstračním příkladu:

-- Paty demonstracni priklad:
-- promenny pocet parametru

-- funkce s dvojici pojmenovanych parametru
-- s moznosti pristupu k dalsim parametrum
-- pristupnym pres vyraz ...
function g(x, y, ...)
    -- pokud je seznam volitelnych parametru
    -- naplnen alespon jednou hodnotou,
    -- vypise se jeho delka
    if ... ~= nil then
        -- prevod na asociativni pole
        local varargs = {...}
        -- vypis delky pole
        print("vararg length: ", #varargs .. " items")
    end
    -- vypis obou pojmenovanych parametru
    -- i promennych parametru
    print(x, y, ...)
end

-- ukazka volani funkce
g()
g(1)
g(1,2)
g(1,2,3)
g(1,2,3,4)
g(1,2,3,5)
g("a", "b", "c", "d", "e")

-- finito 

Po spuštění demonstračního příkladu se na standardní výstup vypíše následující text:

nil             nil
1               nil
1               2
vararg length:  1 items
1               2               3
vararg length:  2 items
1               2               3               4
vararg length:  2 items
1               2               3               5
vararg length:  3 items
a               b               c               d               e 

6. Využití proměnného počtu parametrů

Funkce s proměnným počtem parametrů jsou v některých případech velmi užitečné. V této kapitole si ukážeme, jakým způsobem lze vytvořit funkci nazvanou statistic, která pro zadaný seznam číselných hodnot (seznam může mít libovolnou délku) vrátí uspořádanou trojici: délku předaného seznamu, součet všech číselných hodnot v seznamu (suma) a průměrnou hodnotu vypočtenou na základě sumy a zjištěné délky seznamu. Povšimněte si zejména způsobu využití výrazu . Nejprve je provedeno porovnání, zda není tento výraz roven hodnotě nil – to by totiž znamenalo, že při volání funkce nebyly předány žádné parametry a výpočet průměru nemůže být z tohoto důvodu proveden (došlo by k dělení nulou). Následně je použita jazyková konstrukce items = {…}, pomocí níž se seznam předávaných hodnot přetransformuje na regulární asociativní pole, jehož klíči jsou přirozená čísla. S proměnnou items lze tedy pracovat stejně jako s jakýmkoli jiným asociativním polem, čehož je využito při výpočtu sumy:

-- Sesty demonstracni priklad:
-- vyuziti promenneho poctu parametru

-- funkce vracejici trojici:
-- 1. pocet prvku
-- 2. soucet prvku (suma)
-- 3. aritmeticky prumer jejich hodnot
function statistic(...)
   -- pokud nejsou zadany zadne parametry,
   -- neni nutne vypocet provadet
   if ... == nil then
       return 0, 0, 0
   end
   -- prevod seznamu parametru
   -- na asociativni pole
   items = {...}
   -- pocet prvku
   n = #items
   sum = 0
   -- vypocet sumy
   for i=1, n do
      sum = sum + items[i]
   end
   return n, sum, sum/n
end

print("n", "sum", "average")
print(statistic())
print(statistic(1, 2, 3, 4))
print(statistic(1, 1, 0, 0))
print(statistic(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))

-- finito 
n       sum     average
0       0       0
4       10      2.5
4       2       0.5
10      55      5.5 

7. Rekurze, tail rekurze

V programovacím jazyce Lua je (samozřejmě) podporována i rekurze, tj. z nějaké funkce je možné přímo či nepřímo volat tutéž funkci. Pro zápis rekurze není zavedena žádná speciální syntaxe, pouze dostačuje přímo do těla funkce zapsat volání téže funkce (Pascal naproti tomu vyžaduje použití předběžné deklarace funkce s klíčovým slovem forward, jazyky C a C++ potřebují ze stejného důvodu jako Pascal znát hlavičky funkcí atd.). Samotná podpora rekurze samozřejmě není nic překvapivého, spíš by bylo podivné, kdyby nějaký moderní programovací jazyk tuto vlastnost nepodporoval. Ovšem Lua dokáže, podobně jako Lisp či Scheme, v některých případech klasickou rekurzi, kdy se při každém rekurzivním volání ukládají parametry, lokální proměnné i návratová hodnota na zásobník (jedná se o takzvaný aktivační záznam funkce), nahradit tail rekurzí, při níž se velikost zásobníku nezvětšuje, protože parametry jsou předány přes lokální proměnné a návratová adresa není zpracovávána vůbec (ve své podstatě zde dojde k „rozbalení“ rekurze na smyčku).

Užitečnost tail rekurze spočívá v tom, že se pomocí ní dají snadno vyjádřit některé rekurzivní algoritmy, ovšem samotná cena, kterou zaplatíme za použití rekurze (měřená jako velikost obsazeného místa na zásobníku) je stejná, jako kdyby byly použity klasické cykly (programové smyčky). Aby byla tail rekurze skutečně použita, je nutné rekurzivní volání umístit do posledního příkazu return a navíc musí volající funkce vrátit přímo výsledek funkce volané, bez jeho dalších úprav (například k výsledku není možné přičíst ani vynásobit konstantu). To je poměrně přísná podmínka, která se v dalších jazycích podporujících tail rekurzi většinou nevyskytuje – překladač Lua totiž prozatím nedokáže rozpoznat všechny situace, ve kterých by mohl tail rekurzi skutečně uplatnit.

V demonstračním příkladu na rekurzi jsou vytvořeny dvě rekurzivní funkce. První slouží k rekurzivnímu výpočtu faktoriálu (jedná se o typický školní příklad), druhá pak k výpočtu binomického koeficientu „n nad k“, tj. celkového počtu kombinací k prvků ze sady n prvků. Známý vzorec pro výpočet binomického koeficientu: n!/((n-k)!k!) není pro přímou programovou implementaci příliš vhodný, protože hodnota faktoriálu roste tak rychle, že i pro relativně malé hodnoty brzy mezivýpočet přesáhne povolený rozsah čísel typu double. Místo toho však lze použít rekurzivní vztah: (n nad k)=n/k×(n-1 nad k-1) s tím, že (n nad 0) = 1. Právě tento vztah je převeden do algoritmu v demonstračním příkladu:

-- Sedmy demonstracni priklad:
-- vyuziti rekurze

-- funkce pro rekurzivni vypocet faktorialu
function factorial(n)
    -- faktorial je definovan pouze pro prirozena cisla a nulu
    if n < 0 then
        return nil
    -- rekurze je ukoncena pri n=0
    elseif n == 0 then
        return 1
    else
        return n*factorial(n-1)
    end
end

-- vypis tabulky s faktorialy
print()
print("Factorial")
print("n", "n!")
for n = -5, 10 do
    print(n, factorial(n))
end

-- rekurzivni vypocet binomickeho koeficientu
function binomical(n, k)
    if k == 0 then
        return 1
    else
        return binomical(n - 1, k - 1) * n / k;
    end
end

-- vypis nekterych hodnot "n nad k"
print()
print("Binomical coefficients")
print("n", "k", "n nad k")
for k = 0, 10 do
    for n = k, 10 do
        print(n, k, binomical(n, k))
    end
end

-- finito 

Program po svém spuštění vypíše na standardní výstup text:

bitcoin_skoleni

Factorial
n       n!
-5      nil
-4      nil
-3      nil
-2      nil
-1      nil
0       1
1       1
2       2
3       6
4       24
5       120
6       720
7       5040
8       40320
9       362880
10      3628800

Binomical coefficients
n       k       n nad k
0       0       1
1       0       1
2       0       1
3       0       1
4       0       1
5       0       1
6       0       1
7       0       1
8       0       1
9       0       1
10      0       1
1       1       1
2       1       2
3       1       3
4       1       4
5       1       5
6       1       6
7       1       7
8       1       8
9       1       9
10      1       10
2       2       1
3       2       3
4       2       6
5       2       10
6       2       15
7       2       21
8       2       28
9       2       36
10      2       45
3       3       1
4       3       4
5       3       10
6       3       20
7       3       35
8       3       56
9       3       84
10      3       120
4       4       1
5       4       5
6       4       15
7       4       35
8       4       70
9       4       126
10      4       210
5       5       1
6       5       6
7       5       21
8       5       56
9       5       126
10      5       252
6       6       1
7       6       7
8       6       28
9       6       84
10      6       210
7       7       1
8       7       8
9       7       36
10      7       120
8       8       1
9       8       9
10      8       45
9       9       1
10      9       10
10      10      1 

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

V následující části seriálu o programovacím jazyce Lua si ukážeme, jakým způsobem je možné pracovat s takzvanými uzávěry, což jsou nepojmenované lokální funkce používané v některých programových konstrukcích (především ve funkcionálních jazycích, ke kterým má Lua v několika ohledech velmi blízko). Také si řekneme, jak lze pracovat s funkcemi uloženými v asociativních polích, jelikož právě tímto způsobem uložené funkce tvoří základ objektů (ve smyslu objektově orientovaného programování). Popíšeme si též princip registrace funkcí vytvořených v programovacím jazyce C či C++, které se po svém zaregistrování stávají pro programátory plnohodnotnými Lua–funkcemi, jež lze ze skriptů napsaných v Lua volat, či s nimi dále manipulovat. Uvidíme, že komunikace mezi Lua a céčkovým či C++ programem je poměrně jednoduchá, především při porovnání způsobu registrace a volání externích funkcí v jiných skriptovacích jazycích (ostatně právě tato jednoduchost zapříčinila velkou oblibu jazyka Lua při tvorbě počítačových her).

Autor článku

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