Funktory, funktory aplikatywne, MONoidy Damian Brzuzy, Bartłomiej Lewandowski, Sylwia Maślankowska, Paweł Traczyk, Dawid Szczypiński
Czym się dziś zajmiemy: Funktory Funktory Aplikatywne Nowe Słowa Kluczowe Monoidy
Wstęp W Haskell’u możliwy jest wyższy poziom polimorfizmu niż w innych językach programowania. Typy klas w Haskell’u są otwarte, co oznacza, że możemy zdefiniować własny typ danych, dzięki temu możemy skupić się na tym jak coś ma działać i połączyć to z odpowiednią klasę która definiuje właśnie to działanie. Język Haskell zawiera dużo typów oraz potrafi nawet dopasować właściwy typ po samej deklaracji funkcji. Kolejną własnością Haskell’a jest możliwość deklaracji własnych typów ogólnych i abstrakcyjnych. Powyższe właściwości Haskell’a znalazły swoje zastosowanie w funktorach i monoidach.
FUNKTORY
FunKtorY – Porównanie do MAP Funkcja map to przykład funkcji wyższego rzędu, funkcji której jednym z parametrów jest inna funkcja. Dla funkcji map jest to funkcja która pobiera wartość typu "a" i zwraca wartość typu "b". Funkcja "map" potrzebuje tej funkcji żeby wykonać swoje zadanie, które jest trochę bardziej złożone. Zmienia listę elementów typu "a" na listę elementów typu "b„ Przykład 0. map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = (f x) : (map f xs) ghci>> map (\x -> [-x,x]) [1,2,3] Tak naprawdę, kiedy przyjrzymy się definicji funkcji map dla listy, to sprowadza się ona do jednego. Tworzona jest lista której pierwszym elementem jest wynik zastosowania funkcji f do pierwszego elementu listy wejściowej. Następnie funkcja map stosowana jest do każdego kolejnego elementu listy.
FunKtorY – CO TO JEST? Funktory najprościej można zrozumieć jako odwzorowanie danych wejściowych dowolnego, abstrakcyjnego typu na nowy dowolny abstrakcyjny typ, który najczęściej będzie przedstawiony jako mapy, listy, drzewa, typ wyliczeniowy itp. Funktory charakteryzowane są tylko przez jedną metodę Typeclass, którą jest fmap. Klasa funktora jest zdefiniowana następująco: fmap :: (a -> b) -> fa -> fb Klasa ta jest dostępna z poziomu Prelude, zdefiniowany w import Data.Functor Można to zrozumieć jako sytuacje że mamy funkcje, która pobiera dane typu a i zwraca dane typu b oraz kontener (box) z a oraz kontener (box) z b.
FunKtorY - FMAP Podczas definiowania funktorów można dostarczać dowolne typy, lecz nie można tego brać tak dosłownie. Trzeba wziąć pod uwagę ograniczenia języka Haskell (np. problemy z listami). Funktory oprócz znanych nam typów np. typu wyliczeniowego, listy itp. mogą być zdefiniowane przez swoje własne specjalne typy, zwane instancjami: IO i (->) r Funktory w funkcji fmap mogą zwrócić typ instancji IO, dzięki czemu nie trzeba stosować zmiennych pomocniczych. Od razu możemy zastosować dowolną funkcję na naszych danych wejściowych Powyższe własności funktorów pokazują polimorfizm w języku Haskell
FunKtorY - io Przykład 1. Wykorzystanie funktora działającego na typie instancji IO import Data.Char import Data.List main = do line <- getLine let line' = reverse line putStrLn $ "You said " ++ line' ++ " backwards!" putStrLn $ "Yes, you really said " ++ line' ++ " backwards! " import Data.Char import Data.List main = do line <- fmap reverse getLine putStrLn $ "You said " ++ line ++ " backwards!" putStrLn $ "Yes, you really said" ++ line ++ " backwards!"
FunKtorY - io Przykład 2. Porównanie funktora z funkcją złożoną import Data.Char import Data.List main = do line <- fmap (intersperse '-' . reverse . map toUpper) getLine putStrLn line zzz = (\xs -> intersperse '-' (reverse (map toUpper xs))) Na pierwszy rzut oka widać, że funkcja fmap (funktor) jest bardziej przejrzysta dzięki wykorzystaniu notacji kropkowej Obie implementacje dadzą nam ten sam wynik Dzięki funktorom w łatwy sposób możemy rozbudować naszą funkcję
FunKtorY – sposób działania fmap można użyć na dwa sposoby: fmap które przyjmuje wartość funkcji wejściowej i funktora, a następnie wyświetla wartość funktora za pomocą funkcji pomocniczej np. g (przykłady wcześniej) fmap która przyjmuje funkcję i pracuje nad wartościami funktorów (przykłady poniżej) ghci> fmap (replicate 3) [1,2,3,4] [[1,1,1],[2,2,2],[3,3,3],[4,4,4]] ghci> fmap (replicate 3) (Just 4) Just [4,4,4] ghci> fmap (replicate 3) (Right "blah") Right ["blah","blah","blah"] ghci> fmap (replicate 3) Nothing Nothing ghci> fmap (replicate 3) (Left "foo") Left "foo" fmap w drugim sposobie działania powinien wyświetlać tylko funktor z funkcji i nic więcej.
FunKtorY – PRAWA FUNKTORÓW Wszystkie kopie funktora na których działamy w drugim sposobie ich funkcjonowania powinny przestrzegać „Dwóch Praw Funktorów”. Haskell nie zmusza, aby te prawa wypełniały się automatycznie, dlatego powinno się sprawdzić je podczas tworzenia funktora. Wszystkie typy oparte na kopiach funktorów wbudowane w standardową bibliotekę podlegają tym prawom. Prawo I Pierwsze prawo funktorów stwierdza, że jeśli zastosujemy id funkcji do wartości funktora, który otrzymamy, powinno być ono takie same jak wartość inicjująca funktora. W kilku bardziej formalnych oznaczeniach to, fmap id = id. W istocie stwierdza się, że jeśli zastosujemy id fmap do wartości funktora, to powinno być ono takie samo jakie niesie ze sobą sam funktor. Przypomnijmy sobie, że id - to jest tożsamość, która po prostu zwróci parametr do zmiennej.
FunKtorY – PRAWA FUNKTORÓW Jeśli postrzegamy jaką wartość niesie ze sobą funktor, prawo fmap id = id wygląda na dość banalne i jest oczywiste. Spójrzmy jak to prawo działa dla niektórych wartości funktorów. ghci> fmap id (Just 3) Just 3 ghci> id (Just 3) ghci> fmap id [1..5] [1,2,3,4,5] ghci> id [1..5] ghci> fmap id [] [] ghci> fmap id Nothing Nothing
FunKtorY – PRAWA FUNKTORÓW Jeśli spojrzymy na implementację wbudowanych typów opartych na kopiach funktorów, na przykład dla Maybe, moglibyśmy zrozumieć, dlaczego spełnia się pierwsze prawo: instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing Prawo II Druga zasada mówi, że złożenie dwóch funkcji z użyciem funktora powinno dać ten sam wynik, jak zastosowanie pierwszej funkcji do funktora, a następnie zastosowanie kolejnej funkcji do tego funktora W oficjalnym zapisie oznacza to, że fmap (f . g) = fmap f . fmap g Jeśli chcemy zapisać to dla dowolnej wartości funktora x możemy zapisać: fmap (f . g) x = fmap f (fmap g x) Możemy wyjaśnić, w jaki sposób spełnić drugie prawo w odniesieniu do jakiegokolwiek typu opartego na kopiach funktorów (Maybe, Just)
Funktor może być nawet zupełnie bez sensu, ale będzie funktorem o ile będzie spełniał wspomniane prawa. Funktory mają swoje zastosowanie w instancjach newtype (a one kolejno w monoidach tworząc jedną wielką całość).
FUNKTORY APLIKATYWNE
FunKtorY aplikatywne – Czym są? Są to funktory które są wzbogacone o Typeclass Applicative znajdujący się w module Control.Applicative Funkcje w języku Haskell są domyślnie kalibrowane, co oznacza, że funkcja, która przyjmuje kilka parametrów, faktycznie przyjmuje tylko jeden parametr i zwraca funkcję, która przyjmuje następny parametr i tak dalej. Czyli jeżeli funkcja jest typu a -> b -> c, mówimy, że ma dwa parametry i zwraca c, ale w rzeczywistości dostaje a i zwraca funkcję b -> c. Dlatego możemy wywołać funkcję f x y lub (f x) y. Mechanizm ten umożliwia nam częściowe zastosowanie funkcji, co powoduje, że funkcje możemy przekazać innym funkcjom jako parametr.
FunKtorY aplikatywne – (*) Do tej pory, podczas mapowania funkcji nad funktorami, zazwyczaj mapowaliśmy funkcje, które przyjmują tylko jeden parametr. Ale co się stanie, gdy mapujemy funkcję taką jak *, która przyjmuje dwa parametry przez funktor? (*) traktujemy jako dowolną funkcję Spójrzmy na kilka konkretnych przykładów do tego. Jeśli mamy Just 3 i zrobimy fmap (*) (Just 3) co otrzymamy? Z implementacji instancji Maybe w Funktorach wiemy, że jeśli jest to wartość Just something, zastosuje tę funkcję do something wewnątrz Just. Dlatego wykonanie fmap (*) (Just 3) zwróci Just ((*) 3), który można również zapisać jako Just (* 3), jeżeli używamy sekcji.
FunKtorY aplikatywne – (*) Przykład 7. Użycie (*) ghci> :t fmap (++) (Just "hey") fmap (++) (Just "hey") :: Maybe ([Char] -> [Char]) ghci> :t fmap compare (Just 'a') fmap compare (Just 'a') :: Maybe (Char -> Ordering) ghci> :t fmap compare "A LIST OF CHARS" fmap compare "A LIST OF CHARS" :: [Char -> Ordering] ghci> :t fmap (\x y z -> x + y / z) [3,4,5,6] fmap (\x y z -> x + y / z) [3,4,5,6] :: (Fractional a) => [a -> a -> a] Przykład 8. Użycie (*) ghci> let a = fmap (*) [1,2,3,4] ghci> :t a a :: [Integer -> Integer] ghci> fmap (\f -> f 9) a [9,18,27,36]
FunKtorY aplikatywne – Applicative W przypadku gdy chcemy wykonać mapowanie funktura aplikatywnego na inną wartość funktora automatycznie są wykorzystywane dwie metody pure i <*> które znajdują się w module Control.Applicative. class (Functor f) => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b Z powyżej implementacji wynika że możemy używać fmap jeżeli podczas deklaracji pierwszy parametr będzie funkcją aplikatywną (gwarantuje nam to deklaracja pure)
FunKtorY aplikatywne – Applicative Przykład 10. Prezentację drugiej metody aplicative ghci> Just (+3) <*> Just 9 Just 12 ghci> pure (+3) <*> Just 10 Just 13 ghci> pure (/3) <*> Just 9 Just 3.0 ghci> Just (++"hahah") <*> Nothing Nothing ghci> Nothing <*> Just "woot"
FunKtorY aplikatywne – Applicative Przykład 10. Prezentację pierwszej i drugiej metody aplicative ghci> pure (+) <*> Just 3 <*> Just 5 Just 8 ghci> pure (+) <*> Just 3 <*> Nothing Nothing ghci> pure (+) <*> Nothing <*> Just 5 ghci> [(*0),(+100),(^2)] <*> [1,2,3] [0,0,0,101,102,103,1,4,9] ghci> [(+),(*)] <*> [1,2] <*> [3,4] [4,5,5,6,3,4,6,8]
FunKtorY aplikatywne – Applicative Przykład 11. Prezentację pierwszej metody aplicative w postaci <$> ghci> (++) <$> ["ha","heh","hmm"] <*> ["?","!","."] ["ha?","ha!","ha.","heh?","heh!","heh.","hmm?","hmm!","hmm."] ghci> fmap negate (Just 2) Just (-2) ghci> (\x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 1 [4.0,2.0,0.5] ghci> ((\x y z -> [x, y, z]) <$> (+3) <*> (*2)) 1 2 [4,2,2] Odpowiednik użycia pure … <*> jest <$>
NOWE SŁOWA KLUCZOWE
Nowe słowa kluczowe Słowo kluczowe newtype w Haskell’u jest użyteczne w sytuacjach, w których chcemy po prostu wziąć jeden typ i „opakować” go w coś, aby przedstawić go jako inny typ. Przy użyciu słowa kluczowego newtype można mieć tylko jeden konstruktor wartości, a konstruktor wartości może mieć tylko jedno pole. newtype CharList = CharList { getCharList :: [Char] } deriving (Eq, Show) ghci> :t CharList CharList :: [Char] -> CharList ghci> CharList "this will be shown!" CharList {getCharList = "this will be shown!"} ghci> CharList "benny" == CharList "benny" True ghci> CharList "benny" == CharList "oisters" False
Nowe słowa kluczowe Newtype jest szybszy. Jeśli używasz słowa kluczowego do opakowania typu, w narzędziu jest cały pakiet, który ma zostać opakowany i rozpakowany podczas uruchamiania danego programu. Jeśli jednak użyjesz nowego typu, Haskell wie, że po prostu używasz go do opakowania istniejącego typu do nowego typu (stąd nazwa), ponieważ chcesz, aby był on taki sam, ale różny. Mając to na uwadze, Haskell może pozbyć się owijania i rozpakowywania, gdy tylko ustali, która wartość jest tego typu. Swoje największe zastosowanie znalazły one w monoidach
Nowe słowa kluczowe - fmap Przykład 13. Przykład wykorzystania słowa kluczowego z funktorem newtype Pair b a = Pair { getPair :: (a,b) } instance Functor (Pair c) where fmap f (Pair (x,y)) = Pair (f x, y) ghci> :load src\newtype.hs ghci> getPair $ fmap (*100) (Pair (2,3)) (200,3) ghci> getPair $ fmap reverse (Pair ("london calling", 3)) ("gnillac nodnol",3)
MONOIDY
Monoidy - Wstęp ghci> (3 * 2) * (8 * 5) ghci> 4 * 1 240 4 240 ghci> 3 * (2 * (8 * 5)) ghci> "la" ++ ("di" ++ "da") "ladida" ghci> ("la" ++ "di") ++ "da" ghci> 4 * 1 4 ghci> 1 * 9 9 ghci> [1,2,3] ++ [] [1,2,3] ghci> [] ++ [0.5, 2.5] [0.5,2.5] Na wstępie przed zapoznaniem się z monoidami zauważmy parę faktów Funkcje ++ , * przyjmuje 2 parametry, dlatego musimy używać () aby grupować operacje. Mimo tego grupowania trzeba pamiętać o zasadach z matematyki ponieważ 1/(2*3) to nie to samo co (1/2)*3.
Monoidy - Class Typ klasy monoid: import Data.Monoid class Monoid m where mempty :: m mappend :: m -> m -> m mconcat :: [m] -> m mconcat = foldr mappend mempty m w klasie monoid nie przyjmuje typu parametrycznego mempty jest to funkcja która pełni role polimorficznej stałej utożsamianej z określonym monoidem. mappend jest funkcją binarną pobiera dwie wartości tego samego typu i zwraca ten sam typ. mconcat pobiera listę monoidów i zwraca pojedynczą wartość. To ma trudną implementację, ale po po prostu monoid pobiera jako wartość startową mempty i wykonuję na niej mappend dla kolejnych elementów.
Monoidy Wspominanie na samym wstępie informację mają swoje odzwierciedlenie w prawach monoidów. 3 Prawa monoidów: 1. mempty `mappend` x = x 2. x `mappend` mempty = x 3. (x `mappend` y) `mappend` z = x `mappend` (y `mappend` z)
Monoidy - Listy Monoidy w praktyce: instance Monoid [a] where mempty = [] mappend = (++) ghci> [1,2,3] `mappend` [4,5,6] [1,2,3,4,5,6] ghci> ("one" `mappend` "two") `mappend` "tree" "onetwotree" ghci> "one" `mappend` ("two" `mappend` "tree") ghci> "one" `mappend` "two" `mappend` "tree" ghci> "pang" `mappend` mempty "pang" ghci> mconcat [[1,2],[3,6],[9]] [1,2,3,6,9] ghci> mempty :: [a] []
MonoidY – Product and SUM newtype Product a = Product { getProduct :: a } deriving (Eq, Ord, Read, Show, Bounded) instance Num a => Monoid (Product a) where mempty = Product 1 Product x `mappend` Product y = Product (x * y) ghci> :load src\monoid.hs ghci> getProduct $ Product 3 `mappend` Product 9 27 ghci> getProduct $ Product 3 `mappend` mempty 3 ghci> getProduct $ Product 3 `mappend` Product 4 `mappend` Product 2 24 ghci> getProduct . mconcat . map Product $ [3,4,2]
MonoidY – Product and SUM Wykorzystanie monoidów z fmap w ostatnim przykładzie ghci> getSum $ Sum 2 `mappend` Sum 9 11 ghci> getSum $ mempty `mappend` Sum 3 3 ghci> getSum . mconcat . map Sum $ [1,2,3] 6
MonoidY – any newtype Any = Any { getAny :: Bool } deriving (Eq, Ord, Read, Show, Bounded) instance Monoid Any where mempty = Any False Any x `mappend` Any y = Any (x || y) ghci> :load src\monoid.hs ghci> getAny $ Any True `mappend` Any False True ghci> getAny $ mempty `mappend` Any True ghci> getAny . mconcat . map Any $ [False, False, False, True] ghci> getAny $ mempty `mappend` mempty False
MonoidY – aLL newtype All = All { getAll :: Bool } deriving (Eq, Ord, Read, Show, Bounded) instance Monoid All where mempty = All True All x `mappend` All y = All (x && y) ghci> :load src\monoid.hs ghci> getAll $ mempty `mappend` All True True ghci> getAll $ mempty `mappend` All False False ghci> getAll . mconcat . map All $ [True, True, True] ghci> getAll . mconcat . map All $ [True, True, False]
MonoidY – ORDERING instance Monoid Ordering where mempty = EQ LT `mappend` _ = LT EQ `mappend` y = y GT `mappend` _ = GT ghci> LT `mappend` GT LT ghci> GT `mappend` LT GT ghci> mempty `mappend` LT ghci> mempty `mappend` GT
Monoids – ORDERING import Data.Monoid lengthCompare :: String -> String -> Ordering lengthCompare x y = (length x `compare` length y) `mappend` (x `compare` y) ghci> :load src\monoid.hs ghci> lengthCompare "zen" "ants" LT ghci> lengthCompare "zen" "ant" GT
Monoids – ORDERING import Data.Monoid lengthCompare :: String -> String -> Ordering lengthCompare x y = (length x `compare` length y) `mappend` (vowels x `compare` vowels y) `mappend` (x `compare` y) where vowels = length . filter (`elem` "aeiou") ghci> :load src\monoid.hs ghci> lengthCompare "zen" "anna" LT ghci> lengthCompare "zen" "ana" ghci> lengthCompare "zen" "ann" GT Kolejny przykład monoidu to ramka logiczna XOR w pliku monoid.xor.hs