====== Čisté funkce ====== [[http://en.wikipedia.org/wiki/Pure_function|Čisté funkce]] jsou např. ty, které znáte z matematiky, jako třeba $y=sin(x)$. Čisté funkce: * Vracejí vždy stejné výsledky, pokud jsou volány se stejnými argumenty. (Výsledek funkce nesmí záviset na žádné skryté informaci nebo stavu, které se mohou měnit za běhu programu.) * Nemají žádné vedlejší efekty, jako je modifikace nelokálních proměnných, nebo změna hodnoty předaných argumentů (ani neúmyslně neumožňují, aby taková změna nastala). Čisté funkce mají mnoho výhod, je na nich založen celý styl tzv. [[http://en.wikipedia.org/wiki/Functional_programming|funkcionálního programování]]. ===== Proč všechny funkce nepíšeme jako čisté? ===== Někdy bývá výhodné funkci navrhnout jako tzv. [[http://openbookproject.net/thinkcs/python/english3e/lists.html#pure-functions-and-modifiers|modifikátor]], který obsahuje "vedlejší efekt" záměrně, např. voláním funkce chceme změnit hodnotu argumentu. Modifikátory obvykle nic nevracejí (příp. vracejí jen ''True''/''False'', zda se operace povedla/nepovedla). ===== Souvislost s 2048 ===== V podúloze [[2014_15:2048:3-jeden-krok-hry]] je úkolem vytvořit funkci ''move_tiles()'', který na svém vstupu obdrží aktuální stav herní desky a hráčův tah (sesypat doleva, doprava, nahoru, nebo dolů) a na svém výstupu poskytne výsledný stav hrací desky po sesypání (a přírustek skóre). Funkce má být implementována jako čistá. ==== Problém s měnitelnými argumenty ==== Prvním argumentem funkce je seznam, což je měnitelná (mutable) datová struktura. Co nám v tomto případě hrozí? Představme si, že funkci implementujeme takto (a pro tuto chvíli ignorujme, že funkce dává nesmyslné výsledky): def move_tiles(in_board, direction): return in_board, 0 Tato implementace v podstatě říká, že hrací desky se po žádném tahu nijak nezmění a že skóre za každý tah bude vždy 0. Je takto implementovaná funkce čistá? Koneckonců, pro stejné parametry vrací vždy stejné výsledky, nemodifikuje žádné nelokální proměnné, ani hodnoty měnitelných argumentů... Ale čistá funkce to není, protože skrytě **umožňuje, aby k neočekávané modifikaci argumentu došlo!** Podívejte se např. na tato volání funkce: >>> def move_tiles(in_board, direction): ... return in_board, 0 ... >>> before = [1,2,3,4] >>> after, body = move_tiles(before, 'up') >>> before [1, 2, 3, 4] >>> after [1, 2, 3, 4] Až potud vše v pořádku. Protože funkce stav ''before'' nijak nemodifikuje, je stav ''after'' stejný. Co se ale stane, když v dalším kódu změníme hodnotu stavu ''after''? >>> after[2] = -999 >>> after [1, 2, -999, 4] >>> before [1, 2, -999, 4] Se změnou stavu ''after'' se změnil i stav ''before'', což je rozhodně **neočekávané chování!** Intuitivně čekáme, že stav ''after'' by měl být po zavolání funkce **nezávislý** na stavu ''before'', ale zde očividně není. Proč se tak děje? Funkce ''move_tiles()'' prostě vzala adresu paměťového místa, kterou dostala jako argument a kde je uložen obsah proměnné ''before'', a vrátila ji jako svou návratovou hodnotu, takže proměnná ''after'' ukazuje na stejné paměťové místo jako ''before''. To lze v Pythonu snadno ověřit: >>> before is after True >>> id(before) 50170496 >>> id(after) 50170496 Operátor ''is'' porovnává identitu dvou objektů a zde vidíme, že ''before'' i ''after'' odkazují na stejný objekt (na stejné paměťové místo). Identitu objektu lze v Pythonu zjitit funkcí ''id()''; vidíme, že identita obou objektů je shodná (konkrétní číslo se bude lišit, důležité je, že jsou shodná). ==== Řešení ==== V tomto případě je nejjednodušším řešením vytvořit si hned na začátku funkce kopii mutovatelného vstupního argumentu pomocí funkce [[https://docs.python.org/3.4/library/copy.html#copy.deepcopy|''copy.deepcopy()'']] a dále pracovat s touto kopií. >>> import copy >>> def move_tiles(in_board, direction): ... board = copy.deepcopy(in_board) ... return board, 0 ... >>> before = [1,2,3,4] >>> after, score = move_tiles(before, 'up') >>> before [1, 2, 3, 4] >>> after [1, 2, 3, 4] >>> after[2] = -999 >>> after [1, 2, -999, 4] >>> before [1, 2, 3, 4] >>> after is before False >>> id(after) 50108976 >>> id(before) 50150056 Je vidět, že po zavolání funkce ukazují ''before'' a ''after'' na dva různé objekty, a změna jednoho tudíž neovlivní druhý.