Co to jest „podnoszenie” w Haskell?

138

Nie rozumiem, co to jest „podnoszenie”. Czy powinienem najpierw zrozumieć monady, zanim zrozumiem, czym jest „winda”? (Ja też zupełnie nie znam monad :) A może ktoś może mi to wyjaśnić prostymi słowami?

GabiMe
źródło
9
Może przydatne, może nie: haskell.org/haskellwiki/Lifting
kennytm

Odpowiedzi:

179

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ń Foowziąć typów numerycznych ( Int, Doubleetc) 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, liftFoo4i 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 liftFoo1pisać

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ć Fooz niego funktor, być może możesz uczynić go funktorem aplikacyjnym. W rzeczywistości, jeśli możesz pisać, liftFoo2to 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ć, liftFoo2możesz to napisać w kategoriach tego. Lub możesz to zaimplementować bezpośrednio i nie zawracać sobie głowy liftFoo2, ponieważ Control.Applicativemoduł zawiera

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

i podobnie są liftAi 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 <$> arg1zwraca 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.

Paul Johnson
źródło
26
Prawdopodobnie warto przypomnieć, że windy powinny przestrzegać standardowych przepisów lift id == idi lift (f . g) == (lift f) . (lift g).
Carlos Scheidegger
13
Windy są rzeczywiście „kategorią lub czymś”. Carlos właśnie wymienił prawa Functora, gdzie idi .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 idi .odnoszą się do funkcji Haskella, które znasz i lubisz).
Dan Burton
3
To powinno brzmieć instance Functor Foo, nie instance Foo Functor, prawda? Sam bym edytował, ale nie jestem w 100% pewien.
amalloy
2
Podnoszenie bez Applicative to = Functor. Mam na myśli 2 możliwości: Functor lub Applicative Functor. Pierwsza podnosi funkcje pojedynczego parametru, drugie funkcje wieloparametrowe. Prawie to wszystko. Dobrze? To nie jest fizyka jądrowa :) po prostu tak to brzmi. Dzięki za świetną odpowiedź przy okazji!
jhegedus
2
@atc: to jest częściowa aplikacja. Zobacz wiki.haskell.org/Partial_application
Paul Johnson,
41

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 liftFoo1typem. W rzeczywistości, jeśli tak liftFoo1, możesz utworzyć Fooprzykł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!

Martijn
źródło
25

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"

liftA2przekształca funkcję zwykłych typów na funkcję tego samego typu, która jest zawinięta w ankietęApplicative , taką jak listy IOitp.

Inna wspólna winda jest liftz 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ę.

yairchu
źródło
13

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

Nasser Hadjloo
źródło
40
Tak, ale ta strona zaczyna się od „Zwykle zaczynamy od (kowariantnego) funktora…”. Niezupełnie przyjazny dla początkujących.
Paul Johnson,
3
Ale „funktor” jest połączony, więc nowicjusz może po prostu go kliknąć, aby zobaczyć, czym jest Funktor. Trzeba przyznać, że linkowana strona nie jest tak dobra. Muszę założyć konto i to naprawić.
jrockway
10
To problem, który widziałem na innych stronach programowania funkcjonalnego; każda koncepcja jest wyjaśniona w kategoriach innych (nieznanych) koncepcji, aż nowicjusz zatoczy pełne koło (i zaokrągli zakręt). To musi mieć coś wspólnego z lubieniem rekursji.
DNA,
2
Głosuj na ten link. Winda łączy jeden świat z drugim.
eccstartup
3
Odpowiedzi takie jak ta są dobre tylko wtedy, gdy już rozumiesz temat.
doubleOrt
-2

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 ai myślę o elementach jak o jagodach na drzewie Tree<a>. Istnieje funkcja fmap, która przyjmuje funkcję konwersji elementów a->bi kontener functor<a>. Odnosi się a->bdo każdego elementu pojemnika, skutecznie go przekształcając functor<b>. Gdy podano tylko pierwszy argumenta->b , fmapczeka na functor<a>. Oznacza to, że a->bsamo 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->bdo konwersji, podczas gdy Monad wymaga od użytkownika zdefiniowania a -> f<b>.

Val
źródło
5
Dałem ci notę, bo „funktor to jakiś pojemnik” to przynęta o smaku trolla. Przykład: funkcje od niektórych rdo typu ( cużyjmy dla odmiany) to Functory. Nie „zawierają” żadnych c. W tym przypadku fmap jest kompozycją funkcji, przyjmując a -> bfunkcję i jedynkę r -> a, aby dać ci nową r -> bfunkcję. Nadal żadnych pojemników, więc gdybym mógł, zanotowałbym to ponownie na ostatnie zdanie.
BMeph
1
Ponadto, fmapjest 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 .
BMeph
1
@BMeph To wait, to expect, to anticipatesą synonimami. Mówiąc „funkcja czeka” miałem na myśli „funkcja przewiduje”.
Val
@BMeph Powiedziałbym, że zamiast myśleć o funkcji jako kontrprzykładzie do idei, że funktory są kontenerami, powinieneś pomyśleć o rozsądnej instancji funktora funkcji jako kontrprzykładu do idei, że funkcje nie są kontenerami. Funkcja to odwzorowanie z domeny na kodomenę, gdzie domena jest iloczynem krzyżowym wszystkich parametrów, przy czym kodomena jest typem wyjściowym funkcji. W ten sam sposób lista jest mapowaniem z Naturals do wewnętrznego typu listy (domena -> kodomena). Stają się one jeszcze bardziej podobne, jeśli zapamiętasz funkcję lub nie zachowasz listy.
średnik
@BMeph jednym z jedynych powodów, dla których listy są traktowane bardziej jak kontener, jest to, że w wielu językach można je modyfikować, podczas gdy tradycyjnie funkcje nie mogą. Ale w Haskell nawet to nie jest sprawiedliwe, ponieważ żadne z nich nie może zostać zmutowane, a oba mogą zostać zmutowane na zasadzie kopiowania: b = 5 : ai f 0 = 55 f n = g noba 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ł.
średnik