Níže uvedený text pochází z prvního vydání. Nad tímto textem se nachází aktuální stav po revizi směřující k druhému vydání.

Funkcionální programování

V této kapitole se podíváme na to, jak jazyk Python podporuje další styl programování, takzvané funkcionální programování. Podobně jako u rekurze jde vyloženě o téma pro pokročilé, které v tomto okamžiku můžete jednoduše ignorovat. Techniky funkcionálního programování mají své použití i v každodenní programátorské praxi. Zastánci funkcionálního programování věří, že představuje od základů lepší způsob vývoje software.

Co to vlastně je funkcionální programování?

Funkcionální programování by se nemělo zaměňovat s příkazovým (nebo také procedurálním) programováním. Nepodobá se ani objektově orientovanému programování. Jde o něco jiného. Ne sice zcela jiného, protože koncepty, se kterými budeme pracovat, patří k známým konceptům programování. Jsou pouze vyjádřeny jiným způsobem. Mírně se liší i základní filozofie způsobu používání těchto konceptů při řešení problémů.

Funkcionální programování je založené na výrazech (ve smyslu matematickém). Funkcionální programování bychom vlastně mohli popsat termínem programování orientované na výrazy, protože se zde vše redukuje právě na výrazy. Asi byste si mohli vzpomenout, že výraz je složen z operací a proměnných takovým způsobem, že jeho výsledkem je jedna hodnota. Takže například x == 5 je boolovský výraz. Zápis 5 + (7 - y) představuje aritmetický výraz. A u zápisu string.search("Ahoj, vy tam!", "Aho") jde o řetězcový výraz. V posledním uvedeném případě jde současně o volání funkce v rámci modulu string. Jak později uvidíme, hrají funkce v rámci funkcionálního programování velmi důležitou roli. (To vás asi podle jména už napadlo.)

S funkcemi se ve funkcionálním programování zachází jako s objekty. To znamená, že jsou uvnitř programu často předávány podobným způsobem, jako se předávají proměnné. Příklady jsme viděli v našich programech s grafickým uživatelským rozhraním, kde jsme jméno funkce přiřazovali atributu command ovládacího prvku Button (tlačítko). S funkcí pro obsluhu události jsme zacházeli jako s objektem a odkaz na ni jsme předávali prvku tlačítka. Myšlenka předávání funkcí v rámci našeho programu je pro funkcionální programování myšlenkou klíčovou.

Funkcionální programy bývají rovněž silně orientovány na práci se seznamy.

Závěrem rekněme, že se funkcionální programování snaží soustředit spíše na co, než jak chceme řešit. To znamená, že funkcionální program by měl spíše popisovat řešený problém, než aby se soustředil na vlastní mechanismus řešení. Jazyků, které o sobě tvrdí, že pracují tímto způsobem, existuje několik. Jedním z nejrozšířenějších je Haskell. Na jeho webovém serveru (www.haskell.org) naleznete řadu článků, které popisují jak filosofii funkcionálního programování, tak samotný jazyk Haskell. (Podle mého osobního mínění jsou zmíněné cíle, jakkoliv jsou chvályhodné, zastánci funkcionálního programování poněkud přeceňovány.)

Struktura čistě funkcionálního programu je definována výrazem, který zachycuje požadovaný cíl. Každý term (viz následující poznámka) daného výrazu je zase vyjádřením určité charakteristiky řešeného problému. Vyhodnocením každého z těchto termů nakonec dospějeme k řešení.

Poznámka překladatele: Term neboli terminální podvýraz je v rámci zápisu výrazu jeho dále nedělitelná součást. Můžeme si zde představit volání pojmenované funkce.

Dobrá. To je tedy teoretický pohled. A funguje to vůbec? Ano, někdy to funguje velmi dobře. Pro řešení určitých typů problémů jde o přirozenou a mocnou techniku. Pro řadu dalších problémů tento přístup naneštěstí vyžaduje poměrně abstraktní způsob myšlení, který je velmi ovlivněn matematickými principy. Výsledný kód je pro laického programátora často velmi nečitelný. Ale výsledný kód bývá často mnohem kratší a spolehlivější, než odpovídající kód zapsaný příkazovým stylem. A právě posledně zmiňované kvality přitáhly ke studiu funkcionálního programování mnoho programátorů, kteří používali procedurální nebo objektově orientovaný způsob programování. Dokonce i když se mu nebudete věnovat s plným nasazením, poskytuje funkcionální programování několik mocných nástrojů, které mohou využívat všichni.

Jak se k tomu staví jazyk Python?

Python poskytuje několik funkcí, které umožňují uplatnění funkcionálního přístupu k programování. Tyto funkce patří k rysům, které byly zavedeny hlavně kvůli usnadnění práce, protože bychom je v jazyce Python mohli docela snadno zapsat sami. Mnohem důležitější je ovšem přímé vyjádření záměru, dané jejich existencí. Jde zejména o záměr poskytnout programátorovi v jazyce Python možnost práce ve stylu funkcionálního programování — pokud si přeje jej využít.

Podíváme se na některé z těchto funkcí a uvidíme, jak budou pracovat s některými vzorovými funkcemi, které definujeme jako:

spam = ['pork', 'ham', 'spices']
numbers = [1, 2, 3, 4, 5]

def eggs(item): 
    return item

map(funkce, posloupnost)

Tato funkce aplikuje pythonovskou funkci funkce na každý z členů posloupnosti posloupnost. Mějme výraz:

L = map(eggs, spam)
print L

Výsledem je nový seznam (v tomto případě totožný se seznamem spam), který je vrácen v L.

Stejného efektu bychom mohli dosáhnout zápisem:

for i in spam:
    L.append(i)
print L

Ale povšimněte si skutečnosti, že použitím funkce map() odstraníme potřebu zápisu vnořeného bloku kódu. Z určitého pohledu tím snižujeme složitost programu o jednu úroveň. Uvidíme, že jde o jakési opakující se téma funkcionálního programování. Při používání těchto funkcí se relativní složitost kódu snižuje odstraňováním bloků.

filter(funkce, posloupnost)

Jak již jméno napovídá, funkce filter() vybírá všechny prvky posloupnosti posloupnost, pro které funkce vrací hodnotu True. Uvažujme náš seznam čísel numbers. Pokud chceme vytvořit nový seznam, který se skládá pouze z lichých čísel, pak jej můžeme vytvořit takto:

def jeLiche(n): return (n%2 != 0) # použijeme operátor modulo
L = filter(jeLiche, numbers)
print L

Řešení bychom mohli napsat i takto:

def jeLiche(n): return (n%2 != 0)
for i in numbers:
    if jeLiche(i):
        L.append(i)
print L

Opět si povšimněte, že konvenční kód vyžaduje k dosažení stejného výsledku použití dvou úrovní odsazení. A opět, zvýšení úrovně odsazení je příznakem zvýšené složitosti kódu.

reduce(funkce, posloupnost)

Význam funkce reduce() je poněkud méně zřejmý. Tato funkce redukuje seznam na jedinou hodnotu tím, že jeho prvky kombinuje pomocí dodané funkce. Mohli bychom například sečíst hodnoty prvků seznamu a vrátit hodnotu celkového součtu takto:

def secti(i, j): return i + j
print reduce(secti, numbers)

Stejně jako v předchozích případech bychom mohli použít konvenční zápis:

vysledek = 0
for i in range(len(numbers)): # použijeme indexování
    vysledek = vysledek + numbers[i]
print vysledek

I když v tomto případě bude výsledek stejný, vždycky to není tak přímočaré. Funkce reduce() ve skutečnosti dělá to, že volá dodanou funkci, přičem jí předává první dva členy posloupnosti. Výsledek uloží místo nich. Jinými slovy, přesnější reprezentace funkce reduce() vypadá takto:

def reduce(numbers):
    L = numbers[:]        # vytvoř kopii originálu
    while len(L) >= 2:
        i, j = L[0], L[1] # použijeme násobné přiřazení
        L = [i+j] + L[2:] # součet prvních dvou a zbytek
    return L[0]

Opět vidíme, že technika funkcionálního programování redukuje složitost kódu odstraněním nutnosti použití odsazených bloků kódu.

lambda

Jedním z rysů, kterého jste si v dosud uvedených příkladech mohli všimnout, je skutečnost, že funkce, které předáváme jako argument funkcí pro podporu funkcionálního programování, bývají velmi krátké. Často jde o jediný řádek kódu. Abychom si ušetřili námahu při definování velkého množství velmi malých funkcí, nabízí nám Python další pomocnou funkci pro podporu funkcionálního programování — lambda.

Pojem lambda výraz se používá pro referenci na anonymní funkci, to znamená pro blok kódu, který lze provést stejným způsobem, jako kdyby se jednalo o funkci, která ovšem nemá jméno. Lambda výrazy mohou být definovány uvnitř programu všude tam, kde se může vyskytovat legální pythonovský výraz. To znamená, že je můžeme používat i uvnitř zmiňovaných pomocných funkcí pro podporu funkcionálního stylu programování.

Zápis lambda výrazu vypadá takto:

lambda <seznam parametrů> : <pythonovský výraz používající parametry>

Takže výše zmíněnou funkci secti bychom mohli přepsat jako:

secti = lambda i, j: i + j

Použití řádku s definicí funkce se můžeme úplně vyhnout zápisem lambda výrazu přímo uvnitř volání funkce reduce():

print reduce(lambda i, j: i+j, numbers)

Podobným způsobem můžeme přepsat naše příklady používající funkce map a filter:

L = map(lambda i: i, spam)
print L

L = filter(lambda i: (i%2 != 0), numbers)
print L

List Comprehension

Poznámka překladatele: Pojem list comprehension (čti list komprihenšn) je obtížně přeložitelný tak, aby byl výsledek překladu krátký a přesný. Je založen na jednom z významů slova comprehension, pro který existuje synonymum comprise (čti kemprais). Vyjadřuje skutečnost něco obsahovat, být z něčeho poskládán, složen. Slovo list se zde jednoznačně překládá jako seznam. Pojem list comprehension se používá pro syntaktickou konstrukci, která předepisuje způsob vygenerování seznamu z jiné kolekce. Vulgárně bychom jej tedy mohli přeložit jako sestavovač seznamů. V odborné terminologii by se možná dal použít pojem funkcionální konstruktor seznamu. Ale raději zůstaňme u originálního pojmu. Smiřte se s tím, že naučit se programovat do značné míry znamená také učit se anglicky.

Mechanismus list comprehension představuje techniku pro vytváření nových seznamů, která pochází z jazyka Haskell a v jazyce Python byla uvedena od verze 2.0. Má poněkud zatemňující syntaxi, podobající se matematickému zápisu množin. Vypadá takto:

[<výraz> for <proměnná> in <kolekce> if <podmínka>]

Můžeme ji vyjádřit ekvivalentním zápisem:

L = []
for proměnná in kolekce:
    if podmínka:
        L.append(výraz)

Stejně jako u ostatních konstrukcí z funkcionálního programování i zde dochází k úspoře řádků a dvou úrovní odsazení. Podívejme se na nějaké praktické příklady.

Nejdříve si vytvoříme seznam sudých čísel:

>>> [n for n in range(10) if n % 2 == 0 ]
[0, 2, 4, 6, 8]

Tento zápis říká, že chceme seznam hodnot (n), kde se n pohybuje v rozmezí 0 až 9 a současně platí, že n je sudé (tedy n % 2 == 0).

Podmínka, uvedená na konci, může být samozřejmě nahrazena funkcí — za předpokladu, že tato funkce vrací hodnotu, kterou může Python interpretovat jako hodnotu boolovskou. Takže pokud se znovu podíváme na předchozí příklad, můžeme jej přepsat následovně:

>>> def jeSude(n): return ((n % 2) == 0)
>>> [ n for n in range(10) if jeSude(n) ]
[0, 2, 4, 6, 8]

Vytvořme nyní seznam druhých mocnit prvních pěti čísel:

>>> [n * n for n in range(5)]
[0, 1, 4, 9, 16]

Povšimněte si, že část if nemusíme povinně použít. Úvodní výraz zde má podobu n * n a používáme pro něj všechny hodnoty ze zadaného intervalu.

Na závěr použijme místo funkce range() již existujíci kolekci:

>>> hodnoty = [1, 13, 25, 7]
>>> [x for x in hodnoty if x < 10]
[1, 7]

Takový zápis bychom mohli použít jako náhradu za funkci filter:

>>> filter(lambda x: x < 10, hodnoty)
[1, 7]

To, který ze stylů se zdá přirozenější nebo vhodnější, je čistě subjektivní věc. Pokud budujeme novou kolekci z již existující kolekce, můžeme použít buď dříve uvedené funkce z oblasti funkcionálního programování, nebo můžeme použít list comprehension. Pokud vyloženě vytváříme úplně novou kolekci, pak je obvykle snazší použít list comprehension.

Když to shrneme, tak ačkoliv se nám tyto konstrukce mohou zdát přitažlivé, může pro dosažení požadovaného výsledku tento přístup vést k tak složitým výrazům, že je prostě jednodušší rozepsat je na tradiční pythonovské ekvivalenty. Není to žádná ostuda. Dobrá čitelnost je vždy lepší, než zatemňování věcí jen proto, abychom vypadali chytře.

Ostatní konstrukce

Zmíněné funkce jsou samozřejmě užitečné samy o sobě, ale přesto nestačí k tomu, aby v jazyce Python umožnily plně funkcionální styl programování. Funkcionálním způsobem musíme změnit, nebo alespoň nahradit, používání řídicích struktur jazyka. Jednou z cest, kterou toho můžeme dosáhnout, je využití vedlejšího efektu toho, jak Python vyhodnocuje boolovské výrazy.

Zkrácené vyhodnocování

Protože Python používá zkrácené vyhodnocování boolovských výrazů, lze některých vlastností těchto výrazů využít. Připomeňme si zkrácené vyhodnocování: Pokud se vyhodnocuje boolovský výraz, začíná vyhodnocování na levé straně výrazu a postupuje směrem vpravo. Vyhodnocování končí v okamžiku, kdy již pro určení konečného výsledku není další vyhodnocování nutné.

Pro zviditelnění zkráceného vyhodnocení použijeme zvláštní postup. Nejdříve nadefinujeme dvě funkce, které nám oznámí, že jsou volány, a poté vrátí hodnotu, odpovídající jejich jménům:

>>> def TRUE():
...   print 'TRUE'
...   return True  # True se ve verzi 2.2 tiskne jako 1
...   
>>>def FALSE():
...   print 'FALSE'
...   return False # False se ve verzi 2.2 tiskne jako 0
...

Nyní je použijeme, abychom zjistili, jak se vyhodnocování boolovských výrazů provádí:

>>> print TRUE() and FALSE()
TRUE
FALSE
0
>>> print TRUE() and TRUE()
TRUE
TRUE
1
>>> print FALSE() and TRUE()
FALSE
0
>>> print TRUE() or FALSE()
TRUE
1
>>> print FALSE() or TRUE()
FALSE
TRUE
1
>>> print FALSE() or FALSE()
FALSE
FALSE
0

Poznámka překladatele: Boolovský typ s hodnotami False a True byl zaveden ve verzi Python 2.2. Jména False a True však pouze pojmenovávala konstanty 0 a 1, což se projevovalo při tisku. Předchozí verze jazyka Python boolovský typ vůbec neznaly a místo boolovských hodnot se používaly hodnoty 0 a 1, jako například v jazyce C. Řada programátorů si přesto definovala konstanty False a True. Od verze 2.3 se boolovský typ stal plnohodnotným zabudovaným typem jazyka. Místo hodnot 0 a 1 příkaz print vytiskne False nebo True. Od verze 2.3 tedy bude výstup vypadat takto:

>>> print TRUE() and FALSE()
TRUE
FALSE
False
>>> print TRUE() and TRUE()
TRUE
TRUE
True
>>> print FALSE() and TRUE()
FALSE
False
>>> print TRUE() or FALSE()
TRUE
True
>>> print FALSE() or TRUE()
FALSE
TRUE
True
>>> print FALSE() or FALSE()
FALSE
FALSE
False

Povšimněte si, že u logického součinu (operátor and) dochází k vyhodnocování druhé části výrazu tehdy a jen tehdy, když nabývá první část výrazu hodnoty True (pravda). Pokud první část nabývá logické hodnoty False (nepravda), pak se druhá část nevyhodnocuje, protože výraz jako celek již nemůže nabýt hodnoty True.

Podobně je tomu u logického součtu (operátor or) v případě, kdy první část nabývá hodnoty True. V takovém případě již druhá část nemusí být vyhodnocována, protože celek musí nabývat hodnoty True.

Při vyhodnocování boolovského výrazu se uplatňuje ještě jeden rys, kterého můžeme využít. Jde o to, že při vyhodnocování výrazu v boolovském kontextu Python nevrací jednoduše hodnotu 1 nebo 0 (nebo True či False). Místo toho se vrací skutečná hodnota výrazu. Takže pokud testujeme řetězec na prázdnost (prázdný řetězec se chápe jako False) takto:

if "Tento retezec neni prazdny": print "Neni prazdny"
else: print "Prazdny retezec"

... Python prostě vrací testovaný řetězec.

Této vlastnosti můžeme využít k realizaci chování, které se podobá větvení. Dejme tomu, že máme například následující úsek kódu:

if TRUE(): print "Je to pravda (True)"
else: print "Je to nepravda (False)"

Můžeme jej nahradit konstrukcí ve stylu funkcionálního programování:

V = (TRUE() and "Je to pravda (True)") or ("Je to nepravda (False)")
print V

Zkuste tento příklad provést a poté nahraďte volání TRUE() voláním FALSE().

To znamená, že jsme při využití zkráceného vyhodnocování boolovských výrazů nalezli způsob, jak z našich programů odstranit konvenční příkazy if/else. Možná si vzpomínáte, že jsme u tématu rekurze zjistili, že rekurzi můžeme použít k nahrazení konstrukce cyklu. Takže kombinací těchto dvou efektů můžeme z našeho programu odstranit všechny konvenční řídicí struktury a nahradit je čistými výrazy. Jde o velký krok směrem možnosti řešení problémů čistě ve stylu funkcionálního programování.

Abychom to vše ukázali na praktickém příkladě, napišme si čistě funkcionálním stylem program pro výpočet faktoriálu, který používá rekurzi místo cyklu a zkrácené vyhodnocování výrazu místo if/else:

def factorial(n):
   return ((n <= 1) and 1) or
          (factorial(n - 1) * n)

To je opravdu celé. Možná to není tak dobře srozumitelné jako konvenčnější pythonovský kód, ale funguje to a jde o funkci zapsanou čistě ve funkcionálním stylu — jde tedy o čistý výraz.

Závěry

V tomto místě se možná pozastavujete nad tím, co je vlastně cílem toho všeho? A nejste sami. I když funkcionální programování přitahuje řadu teoretiků v oblasti výpočetních věd (Computer Science) — a často matematiky —, zdá se, že většina praktických programátorů používá techniky funkcionálního programování velmi zřídka. Navíc používají jakýsi hybridní přístup, kdy tyto techniky kombinují s tradičnějším, příkazovým stylem podle toho, jak to považují za vhodné.

Poznámka překladatele: Podle mého názoru může mít takové používání technik funkcionálního programování příčiny jak subjektivní, tak objektivní. Mezi subjektivní příčiny bych zařadil ten fakt, že se jedná přece jen o téma pro pokročilé. Do hloubky se s ním na vysokoškolské úrovni seznámil jen zlomek těch, kteří pracují jako praktičtí programátoři. Mezi objektivní příčiny bych zařadil skutečnost, že model, na kterém je funkcionální programování založeno, neodpovídá přesně modelu, který se používá pro výstavbu běžných počítačů. Von Neumannovská koncepce počítače s pamětí pro data a pro program a s procesorem, který vykonává instrukce, je většinou mnohem efektivněji využitelná pro programy napsané procedurálním stylem. Jinými slovy, dobře napsaný procedurální program na těchto počítačích bude typicky výkonnější, než dobře napsaný odpovídající program zapsaný ve funkcionálním jazyce. Z matematického (teoretického) pohledu je ale tato skutečnost nevýznamná. Proto se řada matematicky zaměřených programátorů, kteří typicky neřeší problémy denní praxe, otázkami skutečného výkonu příliš nezabývá. Funkcionální přístup je z hlediska matematického určitě jednodušší, lépe uchopitelný matematickými nástroji.

Pokud musíme na prvky seznamu aplikovat operace, které lze přirozeně vyjádřit pomocí map, reduce nebo filter, pak je v každém případě použijte. Občas můžete dokonce zjistit, že rekurze je v daném případě vhodnější, než konvenční cyklus. V ještě řidších případech můžete najít použití i pro zkrácené vyhodnocování místo konvenčního if/else — zejména při použití uvnitř výrazu. Tak jako je tomu u každého programátorského nástroje, nenechte se unést jen určitou filozofií. Pro řešení konkrétního zadání se raději snažte použít nejvhodnější nástroj, ať už je jakéhokoliv charakteru. Přinejmenším si buďte vědomi toho, že existují alternativy.

K otázce lambda výrazů zbývá poznamenat ještě jeden závěr. I když neuvažujeme ve stylu funkcionálního programování, existuje jedna oblast, ve které lze dobře použít operátor lambda. Jde o definice funkcí pro obsluhu událostí (event handler), se kterými se setkáváme při programování uživatelského rozhraní. Tyto funkce jsou často velmi krátké, případně se v nich volají nějaké rozsáhlejší funkce, kterým se předává několik napevno použitých hodnot argumentů. V těchto případech lze v roli funkce pro obsluhu událostí použít lambda funkci. Tím se vyhneme nutnosti definovat mnoho malých funkcí, které by nám zaplňovaly prostor jmény, použitými jen jednou. Vzpomeňte si, že příkaz lambda vrací objekt typu funkce. Právě tento objekt (funkce) je předán prvku typu widget a je volán v době, kdy se vyskytne příslušná událost. Pokud si vzpomenete, jak se v Tkinter definuje prvek typu Button (tlačítko), pak můžeme lambda použít následovně:

def write(s): print s
b = Button(rodic, text="Stiskni mne", 
           command = lambda : write("Stiskl jsi mne!"))
b.pack()

V tomto případě bychom samozřejmě mohli dosáhnout téhož efektu tím, že bychom definovali přednastavenou hodnotu parametru funkce write(). Atributu command prvku typu Button bychom pak jednoduše přiřadili write (bez použití závorek). Nicméně i v takto jednoduchém případě nám použití lambda přináší tu výhodu, že jediná definice funkce write() může být použita pro více tlačítek tak, že jí přes lambda předáme různé řetězce. To znamená, že můžeme přidat druhé tlačítko takto:

b2 = Button(rodic, text="Nebo mne", 
            command = lambda : write("Stiskl jsi mne. (Tvoje druhe tlacitko.)"))
b2.pack()

Příkaz lamda můžeme použít i u techniky, kdy používáme metodu ovládacího prvku bind(), které jako argument předáváme objekt pro ošetření události:

b3 = Button(rodic, text="Mne taky stiskni")
b3.bind(<Button-1>, lambda ev : write("Stisknuto"))

To je vše, co se zde o funkcionálním programování dozvíte. Pokud se na ně chcete podívat trochu hlouběji, existuje celá řada dalších informačních zdrojů. Některé z nich jsou uvedeny níže.

Ostatní zdroje

Pokud kdokoliv nalezne dobrý odkaz, pošlete mi zprávu prostřednictvím níže uvedeného odkazu. (Poznámka překladatele: Můžete použít odkaz z anglického originálu, ale i z českého překladu. V druhém případě jej autorovi předám.)


Pokud vás napadne, co by se dalo na překladu této kapitoly vylepšit, zašlete e-mail odklepnutím Tím budou do dopisu automaticky vloženy informace o tomto HTML dokumentu.

$Id: cztutfctnl.html,v 1.5 2004/08/31 11:55:13 prikryl Exp $