Functors, Applicative Functors and Monoids Paweł Piątek Łukasz Rochalski Dawid Malenta
Functors Funktory to rzeczy, które mogą być zmapowane, takie jak listy, Maybes, drzewa i tym podobne. W Haskellu są one opisane przez typeclass Functor, który ma tylko jedną metodę typeclass – fmap, która jest typu Czyli fmap przyjmuje jako parametr funkcję (która przyjmuje a i zwraca b) oraz kontener zawierający dowolną liczbę a i zwraca „kontener” zawierający b. Innymi słowy fmap używa funkcję na elemencie „kontenera”. Jeśli chcemy, aby konstruktor typu był instancją Functora, musi być rodzaju * -> *, co oznacza, że jako parametr typu musi przyjmować dokładnie jeden konkretny typ. Na przykład, Maybe może zostać instancją, ponieważ do wytworzenia konkretnego typu potrzebny jest jeden parametr typu, jak Maybe Int lub Maybe String. Jeśli konstruktor typu przyjmuje dwa parametry, jak Either, musimy częściowo zastosować konstruktor typu do momentu, gdy przyjmie on tylko jeden parametr typu.
Instancja Functor IO IO jest instancją klasy Functor. Wynikiem mapowania czegoś z akcją I/O jest akcja I/O, więc używamy składni do do połączenia dwóch akcji i stworzenia nowej. W implementacji fmap tworzymy nową akcję I/O, która najpierw wykonuje oryginalną akcję I/O i wywołuje jej wynik, następnie używamy return do zwrócenia rezultatu. Return to funkcja, która tworzy akcję I/O, która niczego nie robi, a jedynie przedstawia coś jako jej wynik. Typ fmap przy ograniczeniu się do IO, wygląda tak fmapa przyjmuje funkcję i akcję I/O i zwraca nową akcję I/O taką jak poprzednia, z tą różnicą, że funkcja jest zastosowana do jej wyniku.
Przykłady Functor I/O getLine jest akcją I/O , która ma typ IO String i mapowanie reverse na nim daje nam akcję I/O, które pobierze linię od użytkownika, a następnie zastosuje funkcję reverse na jej wyniku.
Functor (->) r Funkcja typu r -> a może być zapisana jako (->) r a, podobnie jak możemy zapisać 2 + 3 jako (+) 2 3. Kiedy patrzymy na to jak (->) r a, widzimy (->) w nieco innym świetle, ponieważ widzimy, że jest to po prostu konstruktor typu, który przyjmuje dwa parametry typu, tak jak Either. Pamiętajmy jednak, że konstruktor typu musi wziąć dokładnie jeden parametr typu, aby mógł stać się instancją Functora. Dlatego (->) nie może być instancją Functora, ale jeśli częściowo zastosujemy ją do (->) r, nie stwarza to żadnych problemów.
Functor (->) r Jeśli zastąpimy wszystkie f, na (->) r, to zobaczymy jak fmap powinno zachowywać się dla tego konkretnego przypadku. Otrzymujemy: Teraz możemy zapisać (->) r a i (-> r b) w notacji infiksowej r -> a i r -> b, tak jak to zwykle robimy z funkcjami. Teraz otrzymujemy: Mapowanie jednej funkcji na drugiej musi zwrócić funkcję. Jak widać fmap użyte na funkcji działa jak złożenie funkcji.
Functor laws Aby coś mogło być functorem, powinno ono przestrzegać niektórych praw. Od wszystkich funktorów oczekuje się, że będą wykazywać pewne właściwości i zachowania. Powinny one niezawodnie zachowywać się tak, jak rzeczy, które można mapować. Wywołanie fmap na funktorze powinno tylko mapować funkcję nad funktorem, nic więcej. Takie zachowanie jest opisane w prawach funktorów. Dwa z nich, powinny przestrzegać wszystkie instancje Funktora. Pierwsze prawo mówi, że jeśli mapujemy funkcję id nad funktorem, to funktor, który jest zwrócony powinien być taki sam jak oryginalny funktor. Jeśli napiszemy to bardziej formalnie, oznacza to, że fmap id = id. Drugie prawo mówi, że komponowanie dwóch funkcji, a następnie mapowanie funkcji wynikowej nad funktorem powinno być takie samo, jak najpierw mapowanie jednej funkcji nad funktorem, a następnie mapowanie drugiej. Czyli dla każdego funktora F: fmap (f . g) F = fmap f (fmap g F).
Applicative functors Applicative typeclass definiuje dwie metody, pure i <*>. Nie zawiera domyślnej implementacji dla nich. Klasa jest zdefiniowana następująco: pure powinno przyjmować wartość dowolnego typu i zwracać applicative functor z tą wartością wewnątrz niego. Funkcja <*> jest rodzajem rozszerzonego fmap. Bierze ona functor który ma w sobie funkcję i inny functor po czym wydobywa funkcję z pierwszego, a następnie używa jej na drugim.
Applicative Maybe Tak jak powiedzieliśmy wcześniej, pure ma coś wziąć i owinąć w applicative functor. Dla Maybe <*> wyodrębnia funkcję z lewej wartości, jeśli jest to Just, i mapuje ją z prawą wartością. Jeśli którykolwiek z parametrów jest Nothing, Nothing jest wynikiem.
Przykłady Applicative Maybe Widzimy, jak pure(+3) i Just (+3) jest w tym przypadku takie samo. Używamy pure, jeśli mamy do czynienia z wartościami Maybe w kontekście applicative (np. używając ich z <*>), w przeciwnym razie używamy Just. Pierwsze cztery linie wejściowe pokazują, w jaki sposób funkcja jest wyodrębniana, a następnie mapowana. Ostatni wiersz jest interesujący, ponieważ staramy się wyodrębnić funkcję z Nothing, a następnie mapować ją na coś, co oczywiście skutkuje Nothing.
Przykłady Applicative Maybe Applicative functors i używanie pozwalają nam przyjąć funkcję, która oczekuje parametrów niekoniecznie zawiniętych w functory i używać tej funkcji do operowania na kilku wartościach, które są w kontekście funktorowym. Funkcja może przyjmować dowolną ilość parametrów, ponieważ jest zawsze częściowo stosowana krok po kroku pomiędzy wystąpieniami <*>.
Funkcja <$> Control.Applicative zawiera funkcję <$>, która działa jak fmap jako operator infiksowy, zdefiniowaną:
Applicative [ ] Listy (a właściwie ich konstruktor typu, []) są funktorami aplikacyjnymi. pure przyjmuje wartość i umieszcza ją w liście. Przy <*> używamy list comprehension, aby pobierać wartości z obu list. Stosujemy każdą możliwą funkcję z lewej listy do każdej możliwej wartości z prawej listy. Wynikowa lista zawiera wszystkie możliwe kombinacje zastosowania funkcji z lewej listy do wartości z prawej listy.
Przykłady Applicative [ ]
Applicative functor laws Jak zwykłe functory, applicative functors mają kilka praw. Najważniejszym z nich jest to, o którym już wspominaliśmy, a mianowicie, że Inne prawa to:
Applicative ZipList Ponieważ jeden typ nie może mieć dwóch instancji dla tego samego typeclass, wprowadzony został typ ZipList a , który ma jeden konstruktor ZipList, który ma tylko jedno pole typu list. <*> przypisuje pierwszą funkcję do pierwszej wartości, drugą do drugiej itd. Odbywa się to za pomocą Ze względu na to jak działa zipWith, powstała lista będzie tak długa jak krótsza z dwóch list. pure bierze wartość i powtarza ją w liście w nieskończoność
Przykłady Applicative ZipList
Słowo kluczowe newtype Słowo kluczowe newtype w Haskell jest stworzone dla przypadków, gdy chcemy wziąć jeden typ i owinąć go w coś, aby zaprezentować go jako inny typ. Zamiast słowa kluczowego data używane jest słowo kluczowe newtype. newtype jest szybsze. Jeśli używamy słowa kluczowego data koszt ogólny uruchamiania programu jest większy przez wyłuskiwanie i ponowe przysłanianie typu. Jeśli używamy newtype, Haskell wie, że używamy go do przysłonięcia istniejącego typu nowym typem. Gdy używamy newtype jesteśmy ograniczeni do jednego konstruktora z jedym polem.
Słowo kluczowe newtype Możemy również użyć słowa kluczowego deriving z newtype, tak samo jak z data. Możemy używać deriving z Eq, Ord, Enum, Bounded, Show i Read. Jeśli wyprowadzamy instancję dla klasy typu, typ, który przysłaniamy musi być w tej klasie typu. Ma to sens, ponieważ newtype po prostu przysłania istniejący typ.
Przykład użycia newtype Jeśli chcemy, aby krotka była instancją Functor w taki sposób, że gdy fmapujemy funkcję nad krotką, zostanie ona zastosowana do pierwszego elementu krotki (czyli z fmap (+3) (1,1) otrzymamy (4,1)), możemy użyć newtype w taki sposób , aby drugi parametr reprezentował typ pierwszej składowej w krotce.
Monoids Monoidem możemy nazwać rzecz gdy ma asocjacyjną funkcję binarną i wartość, która działa tożsamie w odniesieniu do tej funkcji. Gdy coś działa tożsamie w odniesieniu do funkcji, oznacza to, że po wywołaniu tej funkcji z jakąś inną wartością wynik jest zawsze równy tej innej wartości. 1 oznacza tożsamość w odniesieniu do *, a [] oznacza tożsamość w odniesieniu do ++. Klasa typu monoid jest przeznaczona dla typów, które mogą działać jak monoidy.
Monoids mempty reprezentuje wartość id dla danego monoidu. mappend jest funkcją binarną. Przyjmuje dwie wartości i zwraca wartość` tego samego typu. mconcat pobiera listę wartości i redukuje je do pojedynczej wartości poprzez mapowanie elementów listy. Posiada domyślną implementację, która po prostu przyjmuje mempty jako wartość początkową i używa foldr do złożenia listy z prawej strony.
Monoids laws mempty `mappend` x = x x `mappend` mempty = x (x `mappend` y) `mappend` z = x `mappend` (y `mappend` z) Pierwsze dwa oznaczają, że mempty musi działać tożsamie w odniesieniu do mapowania, a trzeci mówi, że mappend musi być asocjacyjny, tzn. że nie ma znaczenia kolejność, w jakiej używamy mappend, aby zredukować kilka wartości do jednej.
Przykłady monoidów Listy Bool Any i All