Funkcionální programování

Logo funkcionálního programovacího jazyka Haskell.

Funkcionální programování je deklarativní programovací paradigma, které chápe výpočet jako vyhodnocení matematických funkcí. Funkcionální programování má své kořeny v lambda-kalkulu, formálním systému vyvinutém v 30. letech k vyšetřování definicí funkcí, jejich aplikace a rekurze. Mnoho funkcionálních programovacích jazyků může být považováno za rozšíření lambda kalkulu.

Výpočtem funkcionálního programu je tedy posloupnost vzájemně ekvivalentních výrazů, které se postupně zjednodušují. Výsledkem výpočtu, pokud se k němu podaří dospět, je výraz v dále nezjednodušitelné normální formě. Program je chápán jako jedna funkce obsahující vstupní parametry mající jediný výstup. Tato funkce pak může být dále rozložitelná na podfunkce.

V praxi je rozdíl mezi matematickou funkcí a představou funkce použité v imperativním programování. Imperativní funkce mohou mít vedlejší účinky, které mění stav programu. Z toho důvodu postrádají referenční transparentnost: Stejné volání může vést k různým návratovým hodnotám v závislosti na stavu vykonávaného programu. Oproti tomu ve funkcionálním kódu návratové hodnoty funkcí záleží pouze na argumentech funkce, a tudíž dvě volání téže funkce se stejnými argumenty vrací vždy stejnou hodnotu. Eliminace vedlejších účinků může zjednodušit analýzu a pochopení chodu programu, což je jednou z klíčových motivací pro vývoj funkčního programování.

V praxi se můžeme setkat jak s čistě funkcionálními jazyky, které striktně vycházejí z teorie (např. FP, Haskell, Miranda, Hope), tak i s hybridními jazyky, které mohou obsahovat i prvky, které jsou v rozporu se základními principy funkcionálního programování. Například často citovaný „typicky“ funkcionální jazyk Lisp je ve skutečnosti jazykem hybridním, neboť umožňuje modifikovat hodnoty již definovaných proměnných. Mezi hybridní jazyky patří rovněž jazyky Standard ML, Scheme, či F#. Dále lze funkcionální jazyky dělit dle typové kontroly na typované (Haskell, F#, Scala, OCaml, Idris, Lean) a netypované (Lisp, Scheme).

Použití

Funkcionální programovací jazyky, především čistě funkcionální, se používají spíše v akademickém než komerčním prostředí. Přesto široké spektrum organizací využívá některé funkcionální programovací jazyky jako např. Erlang, R (statistika), Mathematica (symbolická matematika), Haskell, ML, J a K (finanční analýza) a doménově specifické programovací jazyky jako XQuery/XSLT (XML). Dále jsou funkcionální programovací jazyky důležité pro některá odvětví informatiky, například zabývající se umělou inteligencí, formální specifikací, modelováním nebo rychlým prototypováním.

Historie

Prvopočátky funkcionálních jazyků najdeme již ve 30. letech 20. století. Tehdy profesor matematiky a filosofie na Princeton University Alonzo Church (1903–1995) vytvořil netypovaný lambda kalkul jako matematickou teorii funkcí. K nejznámějším Churchovým vědeckým přínosům patří také tzv. Churchova–Turingova teze o tom, že algoritmus je ekvivalentní pojmu funkce a Churchův teorém z roku 1936 o tom, že aritmetika je nerozhodnutelná.

Lambda kalkul poskytuje teoretickou podporu pro popis funkcí a jejich vyhodnocení. Ačkoliv je to více matematická abstrakce než programovací jazyk, vytváří dnes základy téměř všech funkcionálních jazyků.

Kombinatorická logika je variace lambda kalkulu, kde jsou lambda výrazy nahrazeny omezenou sadou primitivních funkcí – kombinátorů. Vytvořil ji Moses Schönfinkel a Haskell Brooks Curry. Původně ji vytvořili k dosažení čistšího přístupu k základům matematiky. Kombinatorická logika je obecně chápána víc abstraktně než Lambda kalkul a ve vývoji ji předběhla.

Jeden z prvních jazyků, který v sobě zahrnoval funkcionální část, byl LISP, vytvořený Johnem McCarthym pro IBM série 700/7000 vědeckých počítačů na konci 50. let. LISP představil mnoho vlastností, které můžeme najít v nynějších funkcionálních jazycích, ačkoliv LISP je technicky multi-paradigmatický jazyk. Scheme a Dylan byly pozdější pokusy zjednodušit a vylepšit LISP.

Informační procesní jazyk IPL je někdy uváděn jako první počítačový funkcionální jazyk. Je to jazyk pro manipulaci se seznamem znaků. Má svůj generátor funkcí, který se stará o to, aby funkce mohla přijmout funkci jako argument a vzhledem k tomu, že je to assembly-level jazyk, kód může být použit jako data, takže IPL může být považován za higher-order funkční. Každopádně hodně závisí na měnící se struktuře seznamu a podobných přímých vlastnostech.

Kenneth E. Iverson vyvinul programovací jazyk APL na začátku 60. let a popsal ho roku 1962 ve své knize „A programming Language“. APL měl hlavní vliv na programovací jazyk FP Johna Backuse. Na začátku 90. let, vytvořili Iverson a Roger Hui nástupce APL, J programming. Uprostřed let 90. Artur Whitney, který pracoval s Iversonem, vytvořil programovací jazyk K, který se používá v komerčním a finančním průmyslu.

John Backus představil programovací jazyk FP v roce 1977 ve své přednášce Can Programming Be Liberated From the von Neumann Style, když obdržel Turingovu cenu, která se uděluje za významný technický přínos pro oblast výpočetní techniky. Definoval funkcionální programy tak, že následují principy kompozice. Backusovy novinky popularizovaly výzkum funkcionálního programování, přestože zdůrazňovaly functional-level programming spíše než lambda kalkul, který byl spojován s funkcionálním programováním.

V 70. letech vytvořil Robin Milner na universitě v Edinburghu programovací jazyk ML, a David Turner vyvinul jazyk Miranda na universitě v Kentu. ML byl poté pozměněn do několika dialektů, z nichž jsou nyní nejběžnější Objektive Caml a Standard ML. Programovací jazyk Haskell byl uvolněn[kdo?] na konci osmdesátých let jako pokus skloubit dohromady odlišné přístupy, které byly objeveny v průběhu výzkumu funkcionálního programování.

Koncepty

Spousta konceptů a paradigmat je vlastní funkcionálnímu programování a cizí imperativnímu programování (včetně objektově orientovaného programování). Nicméně programovací jazyky jsou často hybridy několika programovacích paradigmat, takže programátoři používající hlavně imperativní mohou též používat některý z konceptů funkcionálního programování.

Funkce vyššího řádu

Funkce jsou higher-order-function, česky funkce vyššího řádu, v případě, kdy mohou převzít nějakou funkci jako argument nebo navrátit funkci jako výsledek. (Derivace a neurčitý integrál jsou toho příkladem v matematice).

V následující ukázce je naznačena funkce vyššího řádu, protože jí je jako parametr předávána funkce zpětného volání.[1]

// Callback metoda, která se předává jako parametr do volané metody na řádku 12
function callbackFunction(){
    console.log('Já jsem zavolaná callBack metoda');
}

// High order function
function higherOrderFunction(calledFunction){
    console.log('Já jsem high order metoda')
    calledFunction()
}

higherOrderFunction(callbackFunction);

V následující ukázce je kód pro výpočet plochy kružnice a jejího obvodu:

// logika na spočítání obvodu kružnice
const calculatePerimeter = function(radius){ 
    const output = [];
    for(let i = 0; i < radius.length; i++){
        const diameter = 2 * Math.PI * radius[i];
        output.push(diameter)
    }
    return output;
}

// logika na spočítání plochy kružnice
const calculateArea = function(radius){ 
    const output = [];
    for(let i = 0; i < radius.length; i++){
        const area = Math.PI * radius[i] * radius[i];
        output.push(area)
    }
    return output;
}

// zavolání výpočtu plochy dvou kružnic, první s poloměrem 1 a druhé s poloměrem 5
console.log(calculateArea([1, 5])); // výstup volání [3.141592653589793, 78.53981633974483]

// zavolání výpočtu obvodu dvou kružnic, první s poloměrem 1 a druhé s poloměrem 5
console.log(calculatePerimeter([1, 5])); // výstup volání [6.283185307, 31.415926536]

Pokud se využije funkce vyššího řádu, může vypadat kód zhruba následovně:

// logika na výpočet plochy kružnice
const getArea = function(radius){
    return Math.PI * radius * radius;
}

// logika na spočítání obvodu kružnice
const getPerimeter = function(radius){
    return 2 * Math.PI * radius;
}

// znovupoužitelná funkce vyššího řádu
// první argument je radius kružnice a druhý jakákoliv logika, která se má uplatnit na radius
// výstup logiky je poté přidán do pole output
const calculate = function(radius, passedLogic){ 
    const output = [];
    for(let i = 0; i < radius.length; i++){
        output.push(passedLogic(radius[i]))
    }
    return output;
}

// zavolání výpočtu plochy dvou kružnic, první s poloměrem 1 a druhé s poloměrem 5
console.log(calculate([1, 5], getArea)); // výstup volání [3.141592653589793, 78.53981633974483]

// zavolání výpočtu obvodu dvou kružnic, první s poloměrem 1 a druhé s poloměrem 5
console.log(calculate([1, 5], getPerimeter)); // výstup volání [6.283185307, 31.415926536]

Hlavní výhodou funkcí vyššího řádu je jejich znovupoužitelnost a zjednodušení čitelnosti kódu. V ukázce je vidět, že díky metodě calculate je pouze jedna funkce na výpočet a samotná logika je předávána v parametru passedLogic.

Kód, napsaný pomocí funkce vyššího řádu je stručný a modulární. Každá funkce dělá svou vlastní práci a nic zde není potřeba opakovat.[1]

Čistě funkcionální

Čistě funkcionální programy nemají žádné vedlejší účinky. To činí jejich chování jednodušším na pochopení a naprogramování. Například výsledek použití čisté funkce na čistý argument nezávisí na pořadí vyhodnocení. V důsledku jazyk, který nemá žádné jiné funkce než čistě funkcionální (jako například Haskell), může použít evaluaci call-by-need. Nicméně ne všechny funkcionální jazyky jsou čistě funkcionální. Jazyky z rodiny Lispu nejsou čistě funkcionální, protože způsobují vedlejší účinky.

Jelikož čistě funkcionální funkce neupravují sdílené proměnné, může jich být paralelně zavoláno více, aniž by se navzájem ovlivňovaly. Tyto funkce jsou proto vláknově bezpečné, a to umožňuje interpretům a kompilátorům používat strategii, při níž jsou argumenty funkce vyhodnocovány současně s tělem funkce (anglicky call-by-future).

Příkladem těchto funkcí jsou např. matematické funkce získávající maximum a minimum z předaného pole hodnot. Tyto funkce také pro stejný vstup vrátí vždy stejný výstup. [2]

Čistě funkcionální programovací jazyky typicky vyžadují referenční průhlednost, což znamená, že pokud dva výrazy mají stejné hodnoty, může být jeden dosazen za druhý v jakémkoliv výrazu bez ovlivnění výsledku. Například:

y = f(x) * f(x);

Pokud je funkce f(x) čistě funkcionální, může kompilátor kód převést a transformovat program takto:

z  = f(x);
y = z * z;

a eliminuje druhé vyhodnocení (pravděpodobně zbytečného volání funkce f(x)). Tato optimalizace se nazývá eliminace společného podvýrazu (common subexpression elimination).

Nicméně jestliže má funkce vedlejší účinek, volání funkce nemůže být eliminováno. Podívejte se na následující program:

y  = random() * random();

Druhé volání random nemůže být eliminováno, protože návratová hodnota se může lišit od předchozího volání. Podobně

y  = printf("x") * printf("x");

nemůže být optimalizováno, i kdyby printf vrátil stejnou hodnotu v obou případech, chybějící druhé volání by způsobilo změnu ve výstupu programu. Většina kompilátorů pro imperativní programovací jazyky detekuje čisté funkce a provádí obecnou eliminaci podvýrazu pro volání čistých funkcí. Předkompilované knihovny většinou nevyhodnocují tuto informaci a tím zabraňují optimalizaci externí funkce. Některé kompilátory, jako například GCC, přidávají výraz explicitně označující externí funkce jako čisté, takže mohou být optimalizovány i za přítomnosti předkompilovaných knihoven.

Rekurze

Opakování je ve funkcionálních jazycích obvykle provedeno pomocí rekurze. Rekurzivní funkce vyvolávají samy sebe, čímž dovolují opakování programu. Koncová rekurze (tail recursion) může být rozpoznána a optimalizována kompilátorem do stejného kódu, který se používá na implementaci opakování, čti cyklu, u imperativních jazyků. Programovací jazyk Scheme standardně vyžaduje další implementaci[kdo?] k rozpoznávání těchto funkcí.

Obecné vzory rekurzí mohou být změněny[kdo?] za použití higher order funkcí, catamorphismus a anamorphismus jsou toho nejzřejmější příklady. Takové higher order funkce hrají obvykle roli podobnou vestavěným kontrolním strukturám, jako jsou smyčky v imperativních programovacích jazycích.

Striktní, nestriktní a líné vyhodnocení

Funkcionální jazyky mohou být kategorizovány podle toho, používají-li striktní nebo nestriktní vyhodnocení, což jsou koncepty, které říkají, jak budou zpracovány argumenty funkce při vyhodnocování výrazu. Pro ilustraci se podívejte na následující dvě funkce f a g.

f:=x^2+x+1
g:=x+y

Následující výraz bude vyhodnocen jednou z těchto cest.

f(g(1, 4))

Výpočet vnitřnější funkce g jako první.

f(g(1, 4))   →   f(1+4)   →   f(5)   →   5^2+5+1   →   31

Výpočet vnější funkce f jako první.

f(g(1, 4))   →   g(1,4)^2+g(1,4)+1   →   (1+4)^2+(1+4)+1   →   5^2+5+1   →   31

V prvním případě se jedná o striktní výpočet, argumenty funkce jsou vyhodnoceny před voláním funkce; vedle toho druhý případ je příklad nestriktního vyhodnocení, kde jsou argumenty přenechány ve funkci nevyhodnocené a volání funkce určuje, kdy budou argumenty vyhodnoceny.

Striktní vyhodnocení je efektivnější. Ve striktním výpočtu je argument počítán jednou, zatímco v „blbě implementovaném“ nestriktním může být počítán vícekrát, jak můžete vidět v příkladu nahoře, kde je funkce g vypočítána vícekrát. Striktní výpočet je také jednodušší implementovat, pokud argumenty předané datové funkci jsou datové hodnoty, v nestriktním výpočtu mohou být argumenty výrazy. A ve výsledku první funkcionální jazyky jako LISP, ISWIM a ML spolu s hodně novými funkcionálními jazyky používají striktní výpočet.

Nicméně jsou tu důvody preferovat nestriktní výpočet. Lambda kalkul poskytuje silnější teoretické základy pro jazyky, které používají nestriktní výpočet. Nestriktní výpočet používají nejvíce definiční jazyky. Například podporuje nekonečné datové struktury jako seznam všech kladných proměnných typu integer nebo všech prvočísel. S nestriktním výpočtem jsou tyto struktury vypočítány pouze v kontextu, kde je vyžadována konečná délka. To vedlo k vývoji líného výpočtu, což je typ nestriktního výpočtu, kde výsledek počátečního výpočtu kteréhokoliv argumentu může být sdílen přes výpočtovou sekvenci. Ve výsledku není argument spočítán nikdy více než jednou. Líný výpočet je používán hlavně línými moderními čistě funkcionálními jazyky jako je Miranda, Clean a Haskell.

Funkcionální programování v nefunkcionálních jazycích

Je možné používat funkcionální styl programování i v jazycích, které nejsou považovány za funkcionální. Některé nefunkcionální jazyky si od funkcionálních jazyků půjčily některé rysy jako higher-order funkce a zpracování seznamů. To dělá jednodušší používání tohoto stylu v těchto jazycích. Funkcionální struktury jako higher-order funkce a zpracování seznamů můžeme implementovat v C++ pomocí knihoven. V C můžeme použít ukazatele na funkce, abychom získali některé z efektů higher-order funkcí, například můžeme implementovat běžnou funkci mapování za použití funkčních ukazatelů. Deklarativní specifické jazyky jako SQL a Lex/Yacc, které nejsou Turing-kompletní, používají některé elementy funkcionálního programování, hlavně při vyvarování se nestálých hodnot.

Porovnání funkcionálního a imperativního programování

Funkcionální programování je velmi odlišné od imperativního programování. Nejvýraznější rozdíl je v tom, že funkcionální programování zabraňuje vedlejším efektům, které jsou používané v imperativním programování k implementování stavů vstupů a výstupů. Čistě funkcionální programování zakazuje vedlejší účinky. Zakázání vedlejších efektů zajišťuje referenční průhlednost, která ulehčuje verifikaci, optimalizaci a paralelizaci programů a ulehčuje psaní automatických nástrojů k provedení těchto procesů.

Higher-order funkce jsou zřídka používány ve starších imperativních jazycích. Kde by tradiční imperativní programování nejspíše použilo smyčku k prozkoumání seznamu, funkcionální styl by často použil higher-order funkci mapování, která převezme jako argument funkci a seznam, aplikuje funkci na každý element seznamu a vrátí seznam výsledků.

Simulování stavu

Některé úlohy se zdají být většinou implementovány pomocí stavu. Čistě funkcionální programování provádí tyto úlohy a vstupně výstupní úlohy (jako je třeba přijmutí uživatelského vstupu a výstup na obrazovku) jinou cestou. Čistě funkcionální jazyk, jako je Haskell, je implementuje za použití monád, pocházejících z teorie kategorií. Monády jsou extrémně silný nástroj a nabízí intuitivní cestu jak modelovat stav (a jiné vedlejší účinky jako například vstupy a výstupy) v imperativním stylu bez ztráty čistoty. Zatímco existující monády jsou jednoduché na použití, pro spoustu lidí je těžké definovat novou monádu (která je občas potřebná pro určité typy knihoven). Alternativní metody jako třeba Hoareho logika a unikátnost byly vytvořeny pro sledování vedlejších efektů v programu. Některé moderní vývojové jazyky používají systém efektů k jednoznačnému zjištění vedlejších efektů.

Záležitosti efektivity

Funkcionální programovací jazyky mají automatické spravování paměti s garbage collection, v kontrastu se staršími programovacími jazyky jako je C a Pascal, které používají explicitní spravování paměti. Funkcionální programovací jazyky jsou náročnější na systémové prostředky. Nicméně spousta imperativních programovacích jazyků jako Java, Perl, Python, Ruby mají také automatickou správu paměti a taky jsou náročné na systémové prostředky.

Efektivita funkcionálních programovací jazyků se v poslední době zlepšila. Programy, které provádí náročné numerické výpočty ve funkcionálních jazycích jako OCaml a Clean, jsou stejně rychlé jako v C. Pro programy, které pracují s velkými maticemi a vícerozměrnými databázemi, byly vymyšleny a rychlostně optimalizovány array-funkcionální jazyky (jako J a K). Přestože čistě funkcionální jazyky jsou obecně považovány za pomalejší, jakýkoliv imperativní algoritmus se dá vyjádřit v těchto jazycích, přinejhorším s (asymptotickým) logaritmickým zpomalením. Navíc neměnnost dat může v mnoha případech vést k větší efektivitě díky tomu, že kompilátor může používat předpoklady, které by byly v imperativních jazycích nejisté.

Programovací styly

Imperativní jazyky směřují k sérii kroků, vykonané programem při provádění akce, kdežto funkcionální programy směřují ke kompozici a poskládání funkcí často bez upřesňujících explicitních kroků. Jednoduchý příklad dvou řešení stejného problému za použití multiparadigmatického jazyka (Python):

# imperative style
target = []
for x in source_list:
    x = G(x)
    x = F(x)
    target.append(x)

Ve funkcionální verzi to vypadá jinak

# functional style
# FP-oriented languages often have standard compose()
compose2 = lambda F, G: lambda x: (F(G(x))
target = map(compose2(F,G), source_list)

V kontrastu k imperativnímu stylu, který popisuje kroky potřebné k vytvoření položky target, funkcionální styl popisuje matematický vztah mezi položkami source_list a target.

Jedná se ale o nepraktické školometné ukázky, v praxi by se ale v pythonu použilo jedno z následujících řešení, kombinující oba přístupy:

# řešení 1
target = []
for x in source_list:
    target.append(F(G(x)))
# řešení 2
target = [F(G(x)) for x in source_list]

Reference

  1. a b Higher Order Functions in JavaScript – Explained with Practical Examples. freeCodeCamp.org [online]. 2023-01-03 [cit. 2023-05-03]. Dostupné online. (anglicky) 
  2. ELLIOTT, Eric. Master the JavaScript Interview: What is a Pure Function? [online]. 2021-08-24 [cit. 2023-05-04]. Dostupné online. (anglicky) 

Související články

Externí odkazy

Média použitá na této stránce