Nie jestem biegły w Haskellu, więc może to być bardzo łatwe pytanie.
Jakie ograniczenie językowe rozwiązuje Rank2Types ? Czy funkcje w Haskell nie obsługują już argumentów polimorficznych?
haskell
types
polymorphism
higher-rank-types
Andrey Shchekin
źródło
źródło
Odpowiedzi:
Robią, ale tylko o randze 1. Oznacza to, że chociaż możesz napisać funkcję, która przyjmuje różne typy argumentów bez tego rozszerzenia, nie możesz napisać funkcji, która używa swojego argumentu jako różnych typów w tym samym wywołaniu.
Na przykład poniższej funkcji nie można wpisać bez tego rozszerzenia, ponieważ
g
jest używana z różnymi typami argumentów w definicjif
:Zauważ, że jest całkowicie możliwe przekazanie funkcji polimorficznej jako argumentu do innej funkcji. Więc coś takiego
map id ["a","b","c"]
jest całkowicie legalne. Ale funkcja może używać go tylko jako monomorficznego. W przykładziemap
używaid
tak, jakby miał typString -> String
. Oczywiście zamiast opcji można też przekazać prostą funkcję monomorficzną danego typuid
. Bez parametru rank2types nie ma możliwości, aby funkcja wymagała, aby jej argument był funkcją polimorficzną, a zatem nie ma również możliwości użycia jej jako funkcji polimorficznej.źródło
f' g x y = g x + g y
. Jego wywnioskowany typ rangi 1 toforall a r. Num r => (a -> r) -> a -> a -> r
. Ponieważforall a
znajduje się poza strzałkami funkcji, wywołujący musi najpierw wybrać typa
; jeśli wybiorąInt
, otrzymamyf' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
, a teraz ustaliliśmyg
argument, aby mógł przyjąć,Int
ale nieString
. Jeśli włączymy,RankNTypes
możemy dodać adnotacjęf'
typuforall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. Nie można go jednak użyć - co by tog
było?Trudno jest zrozumieć polimorfizm wyższego rzędu, jeśli nie studiujesz bezpośrednio Systemu F , ponieważ Haskell został zaprojektowany tak, aby ukryć przed tobą szczegóły w interesie prostoty.
Ale ogólnie rzecz biorąc, ogólny pogląd jest taki, że typy polimorficzne nie mają tak naprawdę
a -> b
formy, jaką mają w Haskell; w rzeczywistości wyglądają tak, zawsze z wyraźnymi kwantyfikatorami:Jeśli nie znasz symbolu „∀”, czyta się go jako „dla wszystkich”;
∀x.dog(x)
oznacza „dla wszystkich x, x jest psem”. „Λ” to duża lambda, używana do abstrahowania od parametrów typu; druga linia mówi, że id jest funkcją, która przyjmuje typt
, a następnie zwraca funkcję sparametryzowaną przez ten typ.Widzisz, w Systemie F nie możesz
id
od razu zastosować takiej funkcji do wartości; najpierw musisz zastosować funkcję Λ do typu, aby uzyskać funkcję λ, którą zastosujesz do wartości. Na przykład:Standardowy Haskell (tj. Haskell 98 i 2010) upraszcza to, ponieważ nie ma żadnego z tego typu kwantyfikatorów, wielkich lambd i aplikacji typu, ale za kulisami GHC umieszcza je za kulisami, gdy analizuje program pod kątem kompilacji. (Sądzę, że to wszystko jest czas kompilacji, bez narzutu czasu wykonania).
Ale automatyczna obsługa tego przez Haskella oznacza, że zakłada ona, że „∀” nigdy nie pojawia się w lewej gałęzi typu funkcji („→”).
Rank2Types
iRankNTypes
wyłącz te ograniczenia i pozwól ci zastąpić domyślne reguły Haskella dotyczące miejsca wstawianiaforall
.Dlaczego chcesz to zrobić? Ponieważ pełny, nieograniczony System F jest niesamowicie potężny i może zrobić wiele fajnych rzeczy. Na przykład ukrywanie typów i modułowość można zaimplementować przy użyciu typów wyższego rzędu. Weźmy na przykład zwykłą starą funkcję następującego typu rzędu-1 (do ustawienia sceny):
Do użytku
f
, dzwoniący najpierw musi wybrać, jakie typy użyć dor
ia
, a następnie dostarczyć argumentu wynikowego typu. Możesz więc wybraćr = Int
ia = String
:Ale teraz porównaj to z następującym typem wyższego rzędu:
Jak działa tego typu funkcja? Cóż, aby go użyć, najpierw określ, jakiego typu chcesz użyć
r
. Powiedz, że wybieramyInt
:Ale teraz
∀a
znajduje się wewnątrz strzałki funkcji, więc nie możesz wybrać, jakiego typu chcesz użyća
; musisz zastosowaćf' Int
funkcję Λ odpowiedniego typu. Oznacza to, że implementacjaf'
wybierze typ do użyciaa
, a nie wywołującyf'
. Wręcz przeciwnie, bez typów o wyższej randze dzwoniący zawsze wybiera typy.Do czego to jest przydatne? Cóż, w rzeczywistości do wielu rzeczy, ale jeden pomysł jest taki, że można go użyć do modelowania rzeczy, takich jak programowanie obiektowe, w którym „obiekty” łączą pewne ukryte dane razem z pewnymi metodami, które działają na tych ukrytych. Na przykład obiekt z dwiema metodami - jedną zwracającą
Int
a drugą zwracającą aString
, można zaimplementować z tym typem:Jak to działa? Obiekt jest zaimplementowany jako funkcja, która ma pewne wewnętrzne dane typu ukrytego
a
. Aby faktycznie użyć obiektu, jego klienci przekazują funkcję „wywołania zwrotnego”, którą obiekt wywoła za pomocą dwóch metod. Na przykład:Tutaj w zasadzie wywołujemy drugą metodę obiektu, taką, której typ jest
a → String
dla nieznanegoa
. Cóż, nieznanemyObject
klientom; ale tych klientów wiem, z podpisem, że będą w stanie zastosować jedną z dwóch funkcji do niego i dostać OSOBĄInt
lubString
.Aby zobaczyć rzeczywisty przykład Haskella, poniżej znajduje się kod, który napisałem, kiedy się uczyłem
RankNTypes
. Implementuje typ o nazwie,ShowBox
który łączy razem wartość jakiegoś ukrytego typu wraz z jegoShow
wystąpieniem klasy. Zauważ, że w przykładzie na dole tworzę listę,ShowBox
której pierwszy element został utworzony z liczby, a drugi ze łańcucha. Ponieważ typy są ukrywane przy użyciu typów o wyższej randze, nie narusza to sprawdzania typów.PS: dla każdego, kto to czyta, kto zastanawiał się, jak to się
ExistentialTypes
dzieje w zastosowaniach GHCforall
, uważam, że powodem jest to, że używa tego rodzaju techniki za kulisami.źródło
exists
słowo kluczowe, możesz zdefiniować typ egzystencjalny jako (na przykład)data Any = Any (exists a. a)
, gdzieAny :: (exists a. a) -> Any
. Używając ∀xP (x) → Q ≡ (∃xP (x)) → Q, możemy wywnioskować, żeAny
może również mieć typforall a. a -> Any
iforall
stąd pochodzi słowo kluczowe. Uważam, że typy egzystencjalne zaimplementowane przez GHC są zwykłymi typami danych, które również zawierają wszystkie wymagane słowniki typeklas (nie mogłem znaleźć odniesienia, aby to potwierdzić, przepraszam).data ApplyBox r = forall a. ApplyBox (a -> r) a
; kiedy dopasujesz wzorzec doApplyBox f x
, otrzymaszf :: h -> r
ix :: h
dla „ukrytego”, ograniczonego typuh
. Jeśli dobrze rozumiem, przypadek słownika typeklas jest tłumaczony na coś takiego:data ShowBox = forall a. Show a => ShowBox a
jest tłumaczony na coś takiegodata ShowBox' = forall a. ShowBox' (ShowDict' a) a
;instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
;show' :: ShowDict a -> a -> String
.Odpowiedź Luisa Casillasa daje wiele świetnych informacji o tym, co oznaczają typy rangi 2, ale omówię tylko jeden punkt, którego nie omówił. Wymaganie, aby argument był polimorficzny, nie tylko pozwala na używanie go z wieloma typami; ogranicza również to, co ta funkcja może zrobić ze swoimi argumentami i jak może wygenerować swój wynik. Oznacza to, że daje dzwoniącemu mniej elastyczność. Dlaczego chcesz to zrobić? Zacznę od prostego przykładu:
Załóżmy, że mamy typ danych
i chcemy napisać funkcję
który przyjmuje funkcję, która ma na celu wybranie jednego z elementów podanej listy i zwrócenie
IO
akcji wystrzeliwania pocisków w ten cel. Moglibyśmy daćf
prosty typ:Problem w tym, że mogliśmy przypadkowo uciec
a wtedy mielibyśmy duże kłopoty! Dający
f
typu polimorficznego rangi 1w ogóle nie pomaga, ponieważ wybieramy typ,
a
kiedy dzwonimyf
, i po prostu specjalizujemy się w nimCountry
i\_ -> BestAlly
ponownie używamy naszego złośliwego oprogramowania . Rozwiązaniem jest użycie typu rangi 2:Teraz funkcja, którą przekazujemy, musi być polimorficzna, więc
\_ -> BestAlly
nie wpisujemy check! W rzeczywistości żadna funkcja zwracająca element nie znajdujący się na podanej liście nie będzie sprawdzać typu (chociaż niektóre funkcje, które wchodzą w nieskończone pętle lub generują błędy i dlatego nigdy nie zwracają).Powyższe jest oczywiście wymyślone, ale odmiana tej techniki jest kluczem do zapewnienia
ST
bezpieczeństwa monady.źródło
Typy o wyższej randze nie są tak egzotyczne, jak wskazywały inne odpowiedzi. Wierz lub nie, ale wiele języków zorientowanych obiektowo (w tym Java i C #!) Je obsługuje. (Oczywiście nikt w tych społecznościach nie zna ich pod przerażająco brzmiącą nazwą „typy o wyższej randze”).
Przykład, który podam, to podręcznikowa implementacja wzorca Visitor, z którego korzystam cały czas w mojej codziennej pracy. Ta odpowiedź nie jest wprowadzeniem do schematu odwiedzin; wiedza ta jest łatwo dostępna gdzie indziej .
W tej głupiej wyimaginowanej aplikacji HR chcemy operować pracownikami, którzy mogą być pełnoetatowymi pracownikami stałymi lub tymczasowymi kontraktorami. Mój preferowany wariant wzorca Visitor (a właściwie ten, którego dotyczy
RankNTypes
) parametryzuje typ powrotu odwiedzającego.Chodzi o to, że wielu odwiedzających z różnymi typami zwrotów może operować na tych samych danych. Oznacza to, że nie
IEmployee
wolno wyrażać opinii na temat tego, coT
powinno być.Chciałbym zwrócić uwagę na typy. Zauważ, że
IEmployeeVisitor
uniwersalnie kwantyfikuje jego typ zwracany, podczas gdyIEmployee
kwantyfikuje go wewnątrz swojejAccept
metody - to znaczy na wyższym poziomie. Tłumaczenie niezgrabnie z C # na Haskell:Więc masz to. Typy o wyższej randze są wyświetlane w języku C # podczas pisania typów zawierających metody ogólne.
źródło
Slajdy z kursu Bryana O'Sullivana z kursu Haskell na Uniwersytecie Stanforda pomogły mi zrozumieć
Rank2Types
.źródło
Dla osób zaznajomionych z językami obiektowymi funkcja wyższego rzędu jest po prostu funkcją ogólną, która jako argument oczekuje innej funkcji ogólnej.
Np. W TypeScript możesz napisać:
Widzisz, jak ogólny typ funkcji
Identify
wymaga funkcji ogólnej tego typuIdentifier
? To sprawia, żeIdentify
funkcja wyższego rzędu.źródło
Accept
ma typ polimorficzny rzędu 1, ale jest to metodaIEmployee
, która sama w sobie ma stopień 2. Jeśli ktoś mi daIEmployee
, mogę go otworzyć i użyć jegoAccept
metody w dowolnym typie.Visitee
klasa, którą wprowadzasz. Funkcjaf :: Visitee e => T e
jest (po usunięciu cugru) zasadniczof :: (forall r. e -> Visitor e r -> r) -> T e
. Haskell 2010 pozwala uciec od ograniczonego polimorfizmu rangi 2, używając takich klas.forall
moim przykładzie nie możesz wypłynąć . Nie mam żadnego odniesienia, ale możesz znaleźć coś w "Scrap Your Type Classes" . Polimorfizm wyższego rzędu może rzeczywiście wprowadzić problemy ze sprawdzaniem typu, ale ograniczony sortowanie implicite w systemie klas jest w porządku.