Haskell: Typeclass vs przekazywanie funkcji

16

Wydaje mi się, że zawsze można przekazywać argumenty funkcji, a nie klasę. Na przykład zamiast definiowania klasy równości:

class Eq a where 
  (==)                  :: a -> a -> Bool

Używanie go w innych funkcjach do wskazywania argumentu typu musi być instancją Eq:

elem                    :: (Eq a) => a -> [a] -> Bool

Czy nie możemy po prostu zdefiniować naszej elemfunkcji bez użycia klasy znaków i zamiast tego przekazać argument funkcji, który wykonuje zadanie?

Mahdix
źródło
2
to się nazywa przekazywanie słownikowe. Możesz traktować ograniczenia typowe jako niejawne argumenty.
Poscat
2
Możesz to zrobić, ale oczywiście o wiele wygodniej jest nie przekazywać funkcji i po prostu używać jej „standardowej” w zależności od typu.
Robin Zigmond
2
Możesz tak to ująć, tak. Twierdzę jednak, że istnieje jeszcze jedna ważna zaleta: możliwość pisania funkcji polimorficznych, które działają na każdym typie, który implementuje określony „interfejs” lub zestaw funkcji. Myślę, że ograniczenia typowe wyrażają to bardzo wyraźnie w sposób, w jaki nie przekazuje dodatkowych argumentów funkcji. W szczególności z powodu (niestety ukrytych) „praw”, które musi spełnić wiele klas. Monad mOgraniczenie mówi dla mnie więcej niż przekazując dodatkowe argumenty funkcji typów a -> m ai m a -> (a -> m b) -> m b.
Robin Zigmond
1
Zobacz także, czy klasy są niezbędne?
luqui
1
TypeApplicationsRozszerzenie pozwala tworzyć niejawny argument, jednoznaczne. (==) @Int 3 5porównuje, 3a 5konkretnie jako Intwartości. Możesz myśleć o @Intkluczu w słowniku funkcji równości specyficznych dla typu, a nie o Intsamej funkcji porównania specyficznej dla danego typu .
chepner

Odpowiedzi:

19

Tak. Nazywa się to „stylem przekazywania słownika”. Czasami, gdy robię pewne szczególnie trudne rzeczy, muszę zeskrobać klasę i przekształcić ją w słownik, ponieważ przekazywanie słowników jest bardziej wydajne 1 , ale często dość kłopotliwe, co sprawia, że ​​prosty koncepcyjnie kod wygląda na dość skomplikowany. Czasami używam stylu przekazywania słownika w językach, które nie są Haskellem, do symulacji klas (ale nauczyłem się, że to zwykle nie jest tak świetny pomysł, jak się wydaje).

Oczywiście, ilekroć występuje różnica w sile ekspresji, następuje kompromis. Chociaż możesz użyć danego interfejsu API na wiele sposobów, jeśli jest napisany przy użyciu DPS, interfejs API otrzymuje więcej informacji, jeśli nie możesz. Jednym ze sposobów jest to w praktyce Data.Set, które polega na tym, że istnieje tylko jeden Ordsłownik dla każdego typu. W Setsklepach jej elementy sortowane według Ord, a jeśli zbudować zestaw z jednego słownika, a następnie wstawiony element używając innego jednego, jak byłoby to możliwe z DPS, można złamać Set„s niezmienne i spowodować jego awarię. Ten problem wyjątkowości można złagodzić za pomocą fantomu egzystencjalnegowpisz, aby zaznaczyć słownik, ale znowu kosztem dość irytującej złożoności interfejsu API. Widoczne jest to również w prawie taki sam sposób w Typeableinterfejsie API.

Bit wyjątkowości nie pojawia się zbyt często. W typowych klasach pisanie kodu jest dla ciebie świetne. Na przykład,

catProcs :: (i -> Maybe String) -> (i -> Maybe String) -> (i -> Maybe String)
catProcs f g = f <> g

który pobiera dwa „procesory”, które pobierają dane wejściowe i mogą dawać dane wyjściowe, i konkatenuje je, spłaszczając Nothing, musiałyby być zapisane w DPS mniej więcej tak:

catProcs f g = (<>) (funcSemi (maybeSemi listSemi)) f g

Zasadniczo musieliśmy przeliterować typ, w którym go ponownie używamy, nawet jeśli już przeliterowaliśmy go w podpisie typu, a nawet to było zbędne, ponieważ kompilator już zna wszystkie typy. Ponieważ istnieje tylko jeden sposób skonstruowania danego Semigrouptypu, kompilator może to zrobić za Ciebie. Ma to efekt typu „odsetki złożone”, gdy zaczynasz definiować wiele instancji parametrycznych i używasz struktury swoich typów do obliczania dla ciebie, jak w Data.Functor.*kombinatorach, i jest to używane z doskonałym skutkiem deriving via, gdy można uzyskać zasadniczo wszystkie „standardowa” struktura algebraiczna twojego typu napisana dla Ciebie.

I nawet nie zaczynaj mnie od MPTC i fundeps, które dostarczają informacje z powrotem do sprawdzania typów i wnioskowania. Nigdy nie próbowałem przekonwertować czegoś takiego na DPS - podejrzewam, że wymagałoby to przekazania wielu dowodów równości typu - ale w każdym razie jestem pewien, że dla mojego mózgu byłoby to o wiele więcej pracy niż byłoby mi wygodnie z.

-

1 U nless użyć reflectionw tym przypadku stają się one równoważne w siłę - ale reflectionmoże być również kłopotliwe w użyciu.

luqui
źródło
Jestem bardzo zainteresowany fundeps wyrażonymi przez DPS. Czy znasz jakieś godne polecenia zasoby na ten temat? W każdym razie bardzo zrozumiałe wyjaśnienie.
Bob
@bob, nie bezpośrednio, ale byłoby ciekawą eksploracją. Może zadać o to nowe pytanie?
luqui
5

Tak. To (zwane przekazywaniem słownika) jest w zasadzie tym, co kompilator robi, by i tak pisać na typach klas. Dla tej funkcji, wykonanej dosłownie, wyglądałoby to trochę tak:

elemBy :: (a -> a -> Bool) -> a -> [a] -> Bool
elemBy _ _ [] = False
elemBy eq x (y:ys) = eq x y || elemBy eq x ys

Połączenia elemBy (==) x xssą teraz równoważne z elem x xs. W tym konkretnym przypadku możesz pójść o krok dalej: eqza każdym razem ma ten sam pierwszy argument, więc możesz sprawić, że osoba dzwoniąca zastosuje to i skończy z tym:

elemBy2 :: (a -> Bool) -> [a] -> Bool
elemBy2 _ [] = False
elemBy2 eqx (y:ys) = eqx y || elemBy2 eqx ys

Połączenia elemBy2 (x ==) xssą teraz równoważne z elem x xs.

...Zaczekaj. To jest po prostu any. (I w rzeczywistości w standardowej biblioteceelem = any . (==) .)

Joseph Sible-Reinstate Monica
źródło
Przekazywanie słownika AFAIU to podejście Scali do kodowania klas typów. Te dodatkowe argumenty można zadeklarować jako, implicita kompilator wstrzyknie je dla ciebie z zakresu.
michid