Odpowiedzi:
Podnoszenie jest bardziej wzorcem projektowym niż koncepcją matematyczną (chociaż spodziewam się, że ktoś tutaj teraz obali mnie, pokazując, jak windy są kategorią lub czymś).
Zwykle masz pewien typ danych z parametrem. Coś jak
data Foo a = Foo { ...stuff here ...}
Załóżmy, że można zauważyć, że wiele zastosowań Foo
wziąć typów numerycznych ( Int
, Double
etc) i zachować konieczności wprowadź kod, który rozpakowuje te numery, dodaje lub pomnaża je, a następnie owija je z powrotem do góry. Możesz to zewrzeć, pisząc raz kod rozpakowywania i zawijania. Ta funkcja jest tradycyjnie nazywana „windą”, ponieważ wygląda tak:
liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c
Innymi słowy, masz funkcję, która przyjmuje funkcję dwuargumentową (taką jak (+)
operator) i zamienia ją w równoważną funkcję dla Foos.
Więc teraz możesz pisać
addFoo = liftFoo2 (+)
Edycja: więcej informacji
Oczywiście można mieć liftFoo3
, liftFoo4
i tak dalej. Jednak często nie jest to konieczne.
Zacznij od obserwacji
liftFoo1 :: (a -> b) -> Foo a -> Foo b
Ale to jest dokładnie to samo, co fmap
. Więc zamiast liftFoo1
pisać
instance Functor Foo where
fmap f foo = ...
Jeśli naprawdę chcesz całkowitej regularności, możesz powiedzieć
liftFoo1 = fmap
Jeśli możesz zrobić Foo
z niego funktor, być może możesz uczynić go funktorem aplikacyjnym. W rzeczywistości, jeśli możesz pisać, liftFoo2
to instancja aplikacyjna wygląda następująco:
import Control.Applicative
instance Applicative Foo where
pure x = Foo $ ... -- Wrap 'x' inside a Foo.
(<*>) = liftFoo2 ($)
(<*>)
Operator Foo ma typ
(<*>) :: Foo (a -> b) -> Foo a -> Foo b
Stosuje opakowaną funkcję do opakowanej wartości. Więc jeśli możesz wdrożyć, liftFoo2
możesz to napisać w kategoriach tego. Lub możesz to zaimplementować bezpośrednio i nie zawracać sobie głowy liftFoo2
, ponieważ Control.Applicative
moduł zawiera
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
i podobnie są liftA
i liftA3
. Ale w rzeczywistości nie używasz ich zbyt często, ponieważ jest inny operator
(<$>) = fmap
Dzięki temu możesz napisać:
result = myFunction <$> arg1 <*> arg2 <*> arg3 <*> arg4
Termin myFunction <$> arg1
zwraca nową funkcję opakowaną w Foo. To z kolei można zastosować do następnego argumentu za pomocą (<*>)
i tak dalej. Więc teraz zamiast mieć funkcję podnoszenia dla każdego rodzaju, masz po prostu łańcuch aplikacji.
lift id == id
i lift (f . g) == (lift f) . (lift g)
.
id
i .
są odpowiednio strzałką identyfikacyjną i kompozycją strzałek w jakiejś kategorii. Zwykle mówiąc o Haskell, kategorią, o której mowa, jest „Haskell”, którego strzałki są funkcjami Haskella (innymi słowy id
i .
odnoszą się do funkcji Haskella, które znasz i lubisz).
instance Functor Foo
, nie instance Foo Functor
, prawda? Sam bym edytował, ale nie jestem w 100% pewien.
Paul i yairchu to dobre wyjaśnienia.
Chciałbym dodać, że podnoszona funkcja może mieć dowolną liczbę argumentów i nie muszą one być tego samego typu. Na przykład możesz również zdefiniować liftFoo1:
liftFoo1 :: (a -> b) -> Foo a -> Foo b
Ogólnie rzecz biorąc, podnoszenie funkcji, które przyjmują 1 argument, jest przechwytywane w klasie typu Functor
, a operacja podnoszenia jest nazywana fmap
:
fmap :: Functor f => (a -> b) -> f a -> f b
Zwróć uwagę na podobieństwo z liftFoo1
typem. W rzeczywistości, jeśli tak liftFoo1
, możesz utworzyć Foo
przykład Functor
:
instance Functor Foo where
fmap = liftFoo1
Ponadto uogólnienie podnoszenia na dowolną liczbę argumentów nazywane jest stylem aplikacyjnym . Nie przejmuj się tym, dopóki nie zrozumiesz podnoszenia funkcji za pomocą ustalonej liczby argumentów. Ale kiedy to zrobisz, Learn you a Haskell zawiera dobry rozdział na ten temat. Typeclassopedia to kolejny dobry dokument, który opisuje funktora i aplikacyjnych (jak również inne zajęcia typu; przewijania w dół do prawego rozdział w tym dokumencie).
Mam nadzieję że to pomoże!
Zacznijmy od przykładu (dla lepszej prezentacji dodano trochę spacji):
> import Control.Applicative
> replicate 3 'a'
"aaa"
> :t replicate
replicate :: Int -> b -> [b]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) => f Int -> f b -> f [b]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> ['a','b','c']
"abc"
liftA2
przekształca funkcję zwykłych typów na funkcję tego samego typu, która jest zawinięta w ankietęApplicative
, taką jak listy IO
itp.
Inna wspólna winda jest lift
z Control.Monad.Trans
. Przekształca monadyczne działanie jednej monady w działanie przekształconej monady.
Ogólnie rzecz biorąc, „lift” podnosi funkcję / akcję do typu „opakowanego” (tak więc oryginalna funkcja zaczyna działać „pod opakowaniem”).
Najlepszym sposobem na zrozumienie tego, monad itp. Oraz zrozumienia, dlaczego są one przydatne, jest prawdopodobnie kodowanie i używanie ich. Jeśli jest coś, co zakodowałeś wcześniej i podejrzewasz, że może to skorzystać (tj. Skróci to kod itp.), Po prostu wypróbuj to, a łatwo zrozumiesz koncepcję.
Podnoszenie to koncepcja, która pozwala przekształcić funkcję w odpowiadającą jej funkcję w innym (zwykle bardziej ogólnym) ustawieniu
zajrzyj na http://haskell.org/haskellwiki/Lifting
Według tego błyszczącą tutorialu , funktorem jest jakiś pojemnik (jak Maybe<a>
, List<a>
albo Tree<a>
że elementy sklepie można z jakiegoś innego typu, a
). Użyłem notacji generycznej Java <a>
dla typu elementu a
i myślę o elementach jak o jagodach na drzewie Tree<a>
. Istnieje funkcja fmap
, która przyjmuje funkcję konwersji elementów a->b
i kontener functor<a>
. Odnosi się a->b
do każdego elementu pojemnika, skutecznie go przekształcając functor<b>
. Gdy podano tylko pierwszy argumenta->b
, fmap
czeka na functor<a>
. Oznacza to, że a->b
samo dostarczanie zamienia tę funkcję na poziomie elementu w funkcję, functor<a> -> functor<b>
która działa na kontenerach. Nazywa się to podnoszeniemfunkcji. Ponieważ kontener nazywany jest także funktorem, Warunkiem wstępnym podnoszenia są raczej Funktory niż Monady. Monady są swego rodzaju „równolegle” do podnoszenia. Obie opierają się na pojęciu Functor i tak robią f<a> -> f<b>
. Różnica polega na tym, że lifting wykorzystuje się a->b
do konwersji, podczas gdy Monad wymaga od użytkownika zdefiniowania a -> f<b>
.
r
do typu ( c
użyjmy dla odmiany) to Functory. Nie „zawierają” żadnych c
. W tym przypadku fmap jest kompozycją funkcji, przyjmując a -> b
funkcję i jedynkę r -> a
, aby dać ci nową r -> b
funkcję. Nadal żadnych pojemników, więc gdybym mógł, zanotowałbym to ponownie na ostatnie zdanie.
fmap
jest to funkcja, a nie „czekać” na cokolwiek; „Kontener” będący funktorem to cały punkt podnoszenia. Ponadto, monady są, jeśli w ogóle, podwójnym pomysłem na podnoszenie: Monada pozwala ci użyć czegoś, co zostało podniesione kilka dodatnich razy, tak jakby zostało podniesione tylko raz - jest to lepiej znane jako spłaszczenie .
To wait
, to expect
, to anticipate
są synonimami. Mówiąc „funkcja czeka” miałem na myśli „funkcja przewiduje”.
b = 5 : a
i f 0 = 55
f n = g n
oba obejmują pseudo-mutację „pojemnika”. Również fakt, że listy są zwykle przechowywane w całości w pamięci, podczas gdy funkcje są zwykle przechowywane jako obliczenia. Ale listy zapamiętywane / monorficzne, które nie są przechowywane między wywołaniami, obalają ten pomysł.