Čisté funkce

Č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. funkcionálního programování.

Proč všechny funkce nepíšeme jako čisté?

Někdy bývá výhodné funkci navrhnout jako tzv. 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 2048: Úloha 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 ''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ý.

 
2016_2017/2048/pure-functions.txt · Last modified: 2016/10/10 09:02 (external edit)