[[https://intranet.fel.cvut.cz/cz/education/bk/predmety/47/03/p4703006.html|B4B36FUP]] [[https://aicenter.github.io/FUP/|Webové stránky předmětu]] ====== Funkcionální jazyky a jejich vlastnosti. Lambda kalkulus, iterativní konstrukty a rekurze. ====== - Čisté funkce (pure functions), jejich výhody a nevýhody. - Rekurzivní funkce, typy rekurze, koncová rekurze (tail recursion). - Reprezentace seznamů a stromů ve Scheme/Racketu. - Víceřádové funkce (higher-order functions), příklady, currying a částečně vyhodnocené funkce, levý a pravý fold. - Funkční (neboli lexikální) uzávěry, příklady. - Lambda kalkulus, alfa-konverze, beta-redukce, evaluační strategie (normal a applicative), normální forma, Church-Rosserova věta, Y-combinator. - Algebraické datove typy (ADT) v Haskellu, příklad rekurzivního ADT. - Typové třídy (type classes) v Haskellu, polymorfismy v Haskellu. - Typové třídy Functor, Applicative functor, Monad a jejich použití. ===== Čisté funkce ===== **Čisté funkce (pure functions), jejich výhody a nevýhody.** Čistá funkce je taková, která (1) vrací vždy stejný výsledek pro stejné hodnoty argumentů a (2) nevytváří vedlejší efekty – tedy nečte ani nemění nic mimo svůj vnitřní rozsah; nesahá na globály, neloguje, nedělá I/O a ani nemutuje objekty předané referencí či pointerem. ==== Výhody čistých funkcí ==== - **Determinismus**: Pro stejné vstupy vždy vrací stejné výstupy, což usnadňuje testování a ladění. - **Snadná kompozice**: Čisté funkce lze snadno kombinovat a vytvářet složitější funkce. - **Lepší optimalizace**: Kompilátory mohou lépe optimalizovat čisté funkce, protože znají jejich chování. - **Paralelizace**: Čisté funkce lze snadno spouštět paralelně, protože nemají vedlejší efekty, které by mohly způsobit konflikty mezi vlákny. - **Možnost memoizace**: Čisté funkce mohou být snadno ukládány do mezipaměti (cache), což zvyšuje výkon při opakovaném volání s těmi samými argumenty. ==== Nevýhody čistých funkcí ==== - **Výkon**: Čisté funkce mohou být méně výkonné, protože nemohou využívat vedlejší efekty. - **Složitost**: Některé úlohy mohou být obtížné nebo neefektivní řešit čistými funkcemi, například operace s I/O nebo stavem. - **Overhead s zachováváním stavu**: Čisté funkce často vyžadují předávání stavu jako argumentů, což může vést k horšímu výkonu a složitějšímu kódu. ===== Rekurze ===== **Rekurzivní funkce, typy rekurze, koncová rekurze (tail recursion).** Rekurzivní funkce je funkce která volá sama sebe. (define (fact n) (cond ((= 0 n) 1) (#t (* n (fact (- n 1)))) ) ) Typy rekurzí: * tail recursion - rekurzivní krok se volá na konci funkce, rekurze se nevětví * head recursion - rekurzivní krok se volá na začátku funkce, nic se před ním neprovádí, rekurze se nevětví * tree recursion - voláme samotnou rekurzi vícekrát * nested recursion - voláme rekurzi jako argument v jiné rekurzi * přímá / nepřímá rekurze - funkce volá sama sebe / volá jinou funkci která volá zpět funkci Tail recursion by například vypadala (sečtení listu): (define (sum list [acc 0]) (if (empty? list) acc (sum (cdr list) (+ acc (car list))) ) ) Tree rekurze (sečtení stromu): (struct node (val kids) #:transparent) (struct leaf (val) #:transparent) (define (value t) (match t [(leaf x) x] [(node x kids) x] ) ) (define (tree-sum tree) (if (leaf? tree) (value tree) (+ (val tree) (sum (map tree-sum (kids tree)) ) ) ) ) ===== Seznamy a stromy ===== **Reprezentace seznamů a stromů ve Scheme/Racketu.** === Pár === Základní stavební blok je **pár (pair)**. Drží dvě hodnoty, které lze získat voláním car (první prvek) / cdr (list bez prvního prvku). (cons #\A 65) => '(#\A . 65) (define p (cons #\A 65)) (car p) => #\A (cdr p) => 65 Páry lze do sebe zanořit: (cons (cons 1 (cons 2 3)) (cons 'b #f)) === Seznam === **Seznam (list)** je sekvence hodnot. Může nabývat libovolné délky a prázdný seznam značíme '() nebo null. Seznam můžeme vztvořit následujícími způsoby: 1. Použitím zanořených párů : ```scheme (cons 1 (cons 2 (cons 3 (cons 4 '())))) => '(1 2 3 4) ``` 2. Funkcí **list**, která bere sekvenci výrazů, a vrátí seznam hodnoty vytvořené vyhodnocením všech těchto výrazů: ```scheme (list 1 2 3 4) => '(1 2 3 4) (list 1 2 (+ 2 1) 4) => '(1 2 3 4) ``` 3. Funkcí **quote**, která, na rozdíl od list, přijme jediný výraz (typu (fn arg1 arg2 ... argN)) a vrátí seznam jeho komponent bez vyhodnocení: ```scheme (quote (1 2 (+ 2 1) 4)) => '(1 2 (+ 2 1) 4) ``` protože se používá často, lze zkrátit: ```scheme (quote exp) --> 'exp ``` 4. Funkcí **quasiquote** - kombinace předchozích funkcí, tedy dovolí nám vzhodnotit jen některé výrazy pomocí funcke **unquote**: ```scheme (quasiquote (* 1 (unquote (+ 1 1)) 2)) => '(* 1 2 2)` ``` 5. Spojením seznamů : ```scheme (append '(1) '(2) '(3 4)) => '(1 2 3 4) ``` Pracovat se seznamy nám dovolují následující funkce: **car** vrací první prvek seznamu `(car (list 1 2 3 4)) => 1` **cdr** vrací seznam bez prvního prvku `(cdr (list 1 2 3 4)) => '(2 3 4)` Další prvky lze získat kompozicí car a cdr `(car (cdr (cdr (list 1 2 3 4)))) => 3` což lze zkrátit na `(caddr (list 1 2 3 4)) => 3` Prázdnost seznamu lze oveřit použitím **null** ` (null? (list 1 2 3 4)) => #f (null? '()) => #t` === Strom === Stromové struktury můžeme reprezentovat za pomocí vnořených seznamů, například uzel lze definovat jako seznam `'(data left right)` který drží data a pravý a levý podstrom. Můžeme si zavést přístupové funkce ```scheme (define get-data car) (define get-left cadr) (define get-right caddr) ``` {{statnice:bakalar:fuptree.png?300}} Tento strom lze reprezentovat následovně, pokud definujeme prázdný strom jako #f: ``` (define btree '(1 (2 (4 (7 #f #f) #f) (5 #f #f)) (3 (6 (8 #f #f) (9 #f #f)) #f))) ``` Můžeme definovat funcki **find**, která bude vracet seznam uzlů, které spňují predikát: ``` (define (find pred tree) (if tree (let* ([data (get-data tree)] [left (find pred (get-left tree))] [right (find pred (get-right tree))] [both (append left right)]) (if (pred data) (cons data both) both)) '())) ``` Potom lze např. použít pro extrakci všech hodnot vět ích než 5: `(find (lambda (x) (> x 5)) btree) => '(7 6 8 9)` Dalším příkladem stromů jsou výrazy: `13, '(+ 1 2 3), '(* (opp -2) (+ 1 2))` {{statnice:bakalar:fupexprtree.png?300}} ``` (eval-expr '(* (opp -2) (+ 1 2))) => (eval-expr '(* 2 3)) => (eval-expr 6) => 6 ``` Definujeme funkci eval-expr, která vyhodnotí výraz zadaný v seznamu: ``` (define (eval-expr e) (if (number? e) e (let ([op (car e)] [children (map eval-expr (cdr e))]) (cond [(eq? op '+) (apply + children)] [(eq? op '-) (apply - children)] [(eq? op '*) (apply * children)] [(eq? op 'opp) (- (car children))])))) ``` Funkce **apply** bere funkci a seznam argumentů a "rozbalí" argumenty: ` (apply + '(1 2 3)) => (+ 1 2 3)` ===== Víceřádové funkce ===== **Víceřádové funkce (higher-order functions), příklady, currying a částečně vyhodnocené funkce, levý a pravý fold.** Víceřádové funkce jsou funkce, které mohou přijímat jiné funkce jako argumenty nebo vracet funkce jako výstup. Příklad v Racketu: ```scheme (define (apply-twice f x) (f (f x)) ) (apply-twice (lambda (x) (+ x 1)) 5) ; => 7 ``` Tato funkce `apply-twice` přijímá funkci `f` a hodnotu `x`, a aplikuje funkci `f` dvakrát na hodnotu `x`. Další příklad který vrací funkci: ```scheme (define (make-adder x) (lambda (y) (+ x y)) ) (define add5 (make-adder 5)) (add5 10) ; => 15 ``` Tato funkce `make-adder` vrací funkci, která přičte k hodnotě `y` hodnotu `x`, která byla předána při vytvoření této funkce. Často zabudované higher-order funkce zahrnují `map`, `filter` a `foldl`/`foldr`. ==== Currying ==== Currying umožňuje převést funkci na sekvenci funkcí, které přijímají jeden argument, technicky definováno jako $\text{curry}(f(x1, x2, ..., xn)->y) = (x1 -> (x2 -> (... -> (xn -> y) ...)))$, kde f. Příklad v Racketu: ```scheme (define (add x y z) (+ x y z)) (define curry-add (curry add)) ; ⬅ jeden identifikátor, ne „=“ ; postupná aplikace: (((curry-add 1) 5) 6) ; => 12 ; můžeš ale přidat víc argumentů naráz: ((curry-add 1 5) 6) ; => 12 ((curry-add 1) 5 6) ; => 12 (curry-add 1 5 6) ; => 12 ``` ==== Částečně vyhodnocené funkce ==== Částečně vyhodnocené funkce jsou funkce, které jsou aplikovány na méně argumentů, než kolik jich očekávají. Výsledkem je nová funkce, která očekává zbývající argumenty. Příklad v haskellu: ```haskell add :: Int -> Int -> Int -> Int add x y z = x + y + z add5 :: Int -> Int -> Int add5 = add 5 add5 10 20 -- => 35 ``` ==== Levý a pravý fold ==== Levý fold (`foldl`) a pravý fold (`foldr`) jsou funkce vyššího řádu, které projdou seznam a postupně kombinují jeho prvky pomocí zadané funkce `f`. === Levý fold (`foldl`) === `foldl` aplikuje funkci zleva doprava a je tail-rekurzivní, takže v přísně vyhodnocovaných jazycích (např. Racket) běží s konstantní hloubkou zásobníku. **Parametry** * `f` – binární funkce `(acc elem) -> nový-acc` * `acc` – počáteční akumulátor * `lst` – seznam **Průběh** 1. Startuje s akumulátorem `acc`. 2. Pro každý prvek `x` ze seznamu volá `(f acc x)` a výsledek uloží do `acc`. 3. Po zpracování posledního prvku vrátí finální `acc`. **Příklad (Racket)** ```scheme #lang racket (require racket/list) (define lst '(1 2 3 4)) (foldl + 0 lst) ; => 10 ``` Syntaktický strom levého foldu: ``` (foldl f acc (list a b c d)) => (foldl f (f acc a) (list b c d)) (foldl f (f (f acc a) b) (list c d)) (foldl f (f (f (f acc a) b) c) (list d)) (foldl f (f (f (f (f acc a) b) c) d) '()) ``` === Pravý fold (foldr) === Pravý fold (foldr) zpracovává seznam zprava doleva. Není tail-rekurzivní, takže na dlouhých seznamech může v přísně vyhodnocovaném prostředí přetéct zásobník. Kroky: 1. Pokud je seznam prázdný, vrátí `acc`. 2. Vezme první prvek `a` a zavolá `(f a (foldr f acc (rest lst)))`. Tím vznikne pravě-asociovaný řetězec volání `f`. Příklad v Racketu: ```scheme (require racket/list) (define lst '(1 2 3 4)) (foldr + 0 lst) ; => 10 ``` Syntaktický strom právého foldu: ``` (foldr f acc (list a b c d)) => f a (foldr f acc (list b c d)) => f a (f b (foldr f acc (list c d))) => f a (f b (f c (foldr f acc (list d)))) => f a (f b (f c (f d acc))) ``` ===== Funkční uzávěry (Lambda funkce) ===== **Funkční (neboli lexikální) uzávěry, příklady.** Volná Proměná (free variables) - proměnná která je použitá uvnitř funkce, ale není její parametr ani není definovaná uvnitř funkce. Funkční uzávěr (closure) - prostředí ve kterém byla funkce vytvořena. Obsahuje všechny definice volných proměnných, které jsou použity v těle funkce. Uzávěr tedy "uzavírá" proměnné, které jsou dostupné v době jejího vytvoření. Například funkce v racketu: ```scheme (define (make-adder x) (lambda (y) (+ x y)) ) (define add5 (make-adder 5)) (add5 10) ; => 15 ``` V tomto případě `make-adder` vytváří closure, který obsahuje v případě `add5` hodnotu `x = 5`. Když zavoláme `add5`, použije tuto hodnotu `x` a přičte ji k `y`. ===== Lambda kalkulus ===== **Lambda kalkulus, alfa‑konverze, beta‑redukce, evaluační strategie (normal a applicative), normální forma, Church‑Rosserova věta, Y‑kombinátor.** Lambda kalkulus je formální systém pro vyjadřování výpočtů pomocí funkcí. Je základem pro mnoho funkcionálních programovacích jazyků, včetně Haskellu a Racketu. Programy v lambda kalkulu jsou tvořeny funkcemi definovanými pomocí *lambda výrazů*. Lambda výraz má tvar ``` term ::= var | func | app func ::= (λ var . term) app ::= term term ``` Každý lambda výraz může být buď proměnná (*var*), funkce (*func*) nebo aplikace (*app*) funkce na argumenty. ### Alfa‑konverze Alfa‑konverze je přejmenování proměnných, aby se zabránilo kolizím jmen, např. $(\lambda x.,x) ;\rightsquigarrow; (\lambda y.,y)$ ### Beta‑redukce Beta‑redukce aplikuje funkci na argument. Formálně $\bigl(\lambda x.,t\bigr);v ;\rightarrow\_{\beta}; t\[,x := v,]$. Příklad: $(\lambda x.,x)(\lambda y.,y) ;\rightarrow\_{\beta}; (\lambda y.,y)(\lambda y.,y) ;\rightarrow\_{\beta}; (\lambda y.,y).$ ### Normální forma a redex * **Normální forma** – výraz, v němž už neexistuje žádný $\beta$‑redex. * **Redex** – podvýraz tvaru $(\lambda x.,t),v$. Velmi často se na vizualizaci lambda kalkulu používají diagramy, které ukazují strukturu výrazů a jejich redukce. Například výraz $(\lambda x.,x)(\lambda y.,y)$ lze zobrazit jako strom: \usetikzlibrary{arrows.meta} \begin{document} \begin{tikzpicture}\[ every node/.style={draw, circle, inner sep=2pt, font=\small}, level distance=15mm, sibling distance=28mm ] \node {@} child { node {$\lambda x$} child { node {$x$} } } child { node {$\lambda y$} child { node {$y$} } }; \end{tikzpicture} \end{document} ==== Evaluační strategie ==== Evaluační strategie určuje, jak se vyhodnocují lambda výrazy. 1. **Normal order** – redukuje vždy nejlevější vnější redex; zaručí normální formu, existuje‑li. 2. **Applicative order** – nejprve redukuje nejvnitřnější redexy; v případě nekonečné rekurze může divergovat, i když normal order by skončil. Příklad rozlišení vnitřního a vnějšího redexu: $(\lambda y.,y)\bigl((\lambda z.,z z),x\bigr),\bigl((\lambda z.,(\lambda a.,a),z),(\lambda y.,(\lambda z.,z),x)\bigr)$ {{statnice\:redexes.d0o4rzl0.png}} ==== Church‑Rosserova věta ==== 1. Má‑li výraz normální formu, je **jedinečná**. 2. Normal‑order evaluace vždy skončí v normální formě, pokud existuje. === Y‑kombinátor === `Y` je **fixpoint combinator**, který umožní definovat rekurzi bez pojmenování. $$ Y \;\triangleq\; \lambda f.,(\lambda x.,f,(x,x)),(\lambda x.,f,(x,x)) $$ * **Fixpoint vlastnost**: $Y,f ;\rightarrow\_{\beta}; f,(Y,f)$. * **Intuice redukce** $$ Y,f \;\rightarrow_{\beta}\; (\lambda x.,f,(x,x)),(\lambda x.,f,(x,x)) \;\rightarrow_{\beta}\; f\Bigl((\lambda x.,f,(x,x)),(\lambda x.,f,(x,x))\Bigr) \;\rightarrow_{\beta}\; f,(Y,f) $$ * **Faktoriál (schematicky, Racket)** ```scheme (define FACT (Y (λfact (λn (if (= n 0) 1 (* n (fact (- n 1)))))))) ``` * **Kde funguje** * *Lazy/normal‑order* (Haskell) – `Y` přímo funguje. * *Strict/applicative* (Racket, JS) – je potřeba `Z`‑kombinátor (lazy fixpoint). > Stručně: **`Y` přidá rekurzi** tím, že každé funkci vrátí její vlastní výsledek jako argument. > > ===== ADT v Haskellu ===== **Algebraické datove typy (ADT) v Haskellu, příklad rekurzivního ADT.** **Algebraické datové typy (ADT)** v Haskellu slouží pro definici vlastních typů. Umožňují vytvářet nové datové struktury kombinováním:: 1. **součtových typů** (sum types, více variant) 2. **součinových typů** (product types, více polí v jedné variantě) Definují se pomocí **data**: ` data Answer = Yes | No | Unknown ` Answer = typový konstruktor Yes, No, Unknown = datové konstruktory Konstruktory začínají velkým písmenem. ``` answers :: [Answer] answers = [Yes,No,Unknown] flip :: Answer -> Answer flip Yes = No flip No = Yes flip Unknown = Unknown ``` === Parametrické ADT === `data Shape = Circle Float | Rect Float Float` **Circle** a **Rect** jsou funkce které konstruují hodnoty typu **Shape** ``` square :: Float -> Shape square n = Rect n n ``` Jeden z nejčastějších datových typů je: `data Maybe a = Nothing | Just a` který potom zaručuje bezpečné operace: ``` safediv :: Int -> Int -> Maybe Int safediv _ 0 = Nothing safediv m n = Just (m `div` n) ``` === Records === Deklarace závislé čistě na pozici bývají nepraktické, proto lze pole pojmenovat: ``` data Person = Person { firstName :: String, lastName :: String, age :: Int, phone :: String, address :: String } ``` === Rekurzivní ADT === Rekurzivní ADT odkazují na sebe sama — umožňují reprezentovat seznamy, stromy, výrazy apod. `data List a = Nil | Cons a (List a) deriving Show` Nil = prázdný seznam Cons = prvek a zbytek seznamu Příklad hodnoty: `Cons 1 (Cons 2 (Cons 3 Nil)) :: Num a => List a` Dalším příkladem je vyhodnocování výrazů: ``` data Expr a = Val a | Add (Expr a) (Expr a) | Mul (Expr a) (Expr a) ``` ===== Typové třídy ===== **Typové třídy (type classes) v Haskellu, polymorfismy v Haskellu.** {{statnice:bakalar:haskell-typeclasses.B75NDeb3.png?600}} ===== Functor, Applicative, Monad ===== **Typové třídy Functor, Applicative functor, Monad a jejich použití.** === Funktor === Funktor je typ , který implementuje funkci $\texttt{fmap}$, tím jsou všechny funktory "mapovatelné". class Functor f where fmap :: (a -> b) -> f a -> f b data Tree a = Tree a [Tree a] deriving Show instance Functor Tree where fmap f (Tree x []) = Tree (f x) [] fmap f (Tree x ts) = Tree (f x) (map (fmap f) ts) Identita: fmap id == id Kompozice: fmap (f . g) == fmap f . fmap g === Applicative funktor === Funktor, který má navíc definovanou operaci $\texttt{<*>}$ (apply) a funkci pure. class (Functor f) => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b instance Applicative Maybe where pure = Just Nothing <*> _ = Nothing (Just f) <*> something = fmap f something === Monáda === Applicative funktor, který má navíc ještě bind. Jedná se o abstrakci nad výpočtem s kontextem (podle typu). Bind ''%%(>>=)%%'' funguje jako zřetězení operací. Operace, která přijímá zabalenou hodnotu s typem a, rozbalí ji, provede nad ní výpočet a vrátí jí zabalenou s typem b. class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a return je to stejné jako pure v applicative.