Osierocone instancje w Haskell

86

Podczas kompilowania mojej aplikacji Haskell z -Wallopcją GHC narzeka na osierocone instancje, na przykład:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

Klasa typu ToSElemnie jest moja, jest zdefiniowana przez HStringTemplate .

Teraz wiem, jak to naprawić (przenieść deklarację instancji do modułu, w którym zadeklarowano Wynik) i wiem, dlaczego GHC wolałby unikać osieroconych instancji , ale nadal uważam, że moja droga jest lepsza. Nie obchodzi mnie, czy kompilator jest niewygodny - raczej to niż ja.

Powodem, dla którego chcę zadeklarować moje ToSElemwystąpienia w module Publisher, jest to, że to moduł Publisher zależy od HStringTemplate, a nie inne moduły. Staram się oddzielić obawy i uniknąć uzależnienia każdego modułu od HStringTemplate.

Pomyślałem, że jedną z zalet klas typów Haskella, w porównaniu na przykład z interfejsami Javy, jest to, że są one otwarte, a nie zamknięte, dlatego instancje nie muszą być deklarowane w tym samym miejscu, co typ danych. Wydaje się, że rada GHC to ignorowanie tego.

Więc to, czego szukam, to albo potwierdzenie, że moje myślenie jest prawidłowe i że miałbym prawo zignorować / stłumić to ostrzeżenie, albo bardziej przekonujący argument przeciwko robieniu rzeczy po swojemu.

Dan Dyer
źródło
Dyskusja w odpowiedziach i komentarzach pokazuje, że istnieje duża różnica między definiowaniem instancji osieroconych w pliku wykonywalnym , tak jak to robisz, a w bibliotece, która jest widoczna dla innych. To niezwykle popularne pytanie ilustruje, jak zagmatwane mogą być przypadki osierocone dla użytkowników końcowych biblioteki, która je definiuje.
Christian Conkle

Odpowiedzi:

94

Rozumiem, dlaczego chcesz to zrobić, ale niestety może to być tylko złudzenie, że zajęcia Haskell wydają się być „otwarte” w sposób, w jaki to mówisz. Wiele osób uważa, że ​​możliwość zrobienia tego jest błędem w specyfikacji Haskell, z powodów, które wyjaśnię poniżej. W każdym razie, jeśli naprawdę nie jest to odpowiednie dla instancji, musisz być zadeklarowany albo w module, w którym zadeklarowano klasę, albo w module, w którym jest zadeklarowany typ, prawdopodobnie jest to znak, że powinieneś użyć newtypelub innego opakowania wokół Twojego typu.

Powody, dla których należy unikać osieroconych instancji, są znacznie głębsze niż wygoda kompilatora. Ten temat jest dość kontrowersyjny, jak widać na podstawie innych odpowiedzi. Aby zrównoważyć dyskusję, wyjaśnię punkt widzenia, że ​​nigdy, przenigdy nie powinno się pisać instancji osieroconych, co, jak sądzę, jest zdaniem większości wśród doświadczonych haskellerów. Moja własna opinia jest gdzieś pośrodku, co wyjaśnię na końcu.

Problem wynika z faktu, że gdy istnieje więcej niż jedna deklaracja instancji dla tej samej klasy i typu, w standardowym Haskellu nie ma mechanizmu określającego, którego użyć. Program jest raczej odrzucany przez kompilator.

Najprostszym skutkiem tego jest to, że możesz mieć doskonale działający program, który nagle przestałby kompilować z powodu zmiany dokonanej przez kogoś innego w jakiejś odległej zależności od twojego modułu.

Co gorsza, działający program może zacząć się zawieszać w czasie wykonywania z powodu odległej zmiany. Możesz użyć metody, która, jak zakładasz, pochodzi z określonej deklaracji instancji i może zostać po cichu zastąpiona inną instancją, która jest po prostu inna na tyle, aby spowodować, że program zacznie się zawieszać w niewytłumaczalny sposób.

Osoby, które chcą mieć gwarancję, że te problemy nigdy się im nie przytrafią, muszą przestrzegać zasady, że jeśli ktokolwiek, gdziekolwiek, kiedykolwiek zadeklarował wystąpienie określonej klasy dla określonego typu, żadne inne wystąpienie nie może być ponownie zadeklarowane w żadnym napisanym programie przez kogokolwiek. Oczywiście istnieje obejście polegające na użyciu a newtypedo zadeklarowania nowej instancji, ale jest to zawsze przynajmniej niewielka niedogodność, a czasem poważna. W tym sensie ci, którzy celowo piszą przypadki osierocone, są raczej niegrzeczni.

Więc co należy zrobić z tym problemem? Obóz anty-osieroconych-instancji twierdzi, że ostrzeżenie GHC jest błędem, musi to być błąd, który odrzuca każdą próbę zadeklarowania osieroconej instancji. W międzyczasie musimy ćwiczyć samodyscyplinę i unikać ich za wszelką cenę.

Jak widzieliście, są tacy, którzy nie przejmują się takimi potencjalnymi problemami. Jak sugerujesz, zachęcają do korzystania z instancji osieroconych jako narzędzia do oddzielania obaw i mówią, że należy po prostu upewnić się, że nie ma problemu w każdym przypadku z osobna. Wystarczająco wiele razy przeszkadzały mi przypadki sierot innych ludzi, aby przekonać się, że taka postawa jest zbyt nonszalancka.

Myślę, że właściwym rozwiązaniem byłoby dodanie rozszerzenia do mechanizmu importu Haskella, które kontrolowałoby import instancji. Nie rozwiązałoby to całkowicie problemów, ale pomogłoby w ochronie naszych programów przed uszkodzeniami powodowanymi przez instancje sieroce, które już istnieją na świecie. A potem, z czasem, mógłbym się przekonać, że w pewnych ograniczonych przypadkach przypadek sieroty może nie być taki zły. (I właśnie ta pokusa jest powodem, dla którego niektórzy w obozie przeciw instancji sierocym są przeciwni mojej propozycji).

Mój wniosek z tego wszystkiego jest taki, że przynajmniej na razie stanowczo radziłbym, abyście unikali ogłaszania jakichkolwiek przypadków osieroconych, aby byli uważni na innych, jeśli nie ma innego powodu. Użyj newtype.

Yitz
źródło
4
W szczególności jest to coraz większy problem przy rozwoju bibliotek. Przy ponad 2200 bibliotekach na Haskell i dziesiątkach tysięcy pojedynczych modułów, ryzyko pobrania instancji dramatycznie rośnie.
Don Stewart
16
Re: „Myślę, że właściwym rozwiązaniem byłoby dodanie rozszerzenia do mechanizmu importu Haskella, które kontrolowałoby import instancji”. Jeśli ten pomysł kogoś interesuje, warto przyjrzeć się na przykład językowi Scala; ma bardzo podobne funkcje do kontrolowania zakresu „implicitów”, których można używać podobnie jak instancji typeklas.
Matt
5
Moje oprogramowanie jest raczej aplikacją niż biblioteką, więc możliwość sprawiania problemów innym programistom jest prawie zerowa. Można by uznać moduł Publisher za aplikację, a pozostałe moduły za bibliotekę, ale gdybym rozprowadzał bibliotekę, byłoby to bez Wydawcy, a zatem bez osieroconych instancji. Ale gdybym przeniósł instancje do innych modułów, biblioteka byłaby dostarczana z niepotrzebną zależnością od HStringTemplate. Więc w tym przypadku myślę, że sieroty są w porządku, ale posłucham twojej rady, jeśli napotkam ten sam problem w innym kontekście.
Dan Dyer
1
To brzmi jak rozsądne podejście. Jedyną rzeczą, na którą należy uważać, jest to, że autor importowanego modułu doda tę instancję w późniejszej wersji. Jeśli to wystąpienie jest takie samo jak Twoje, musisz usunąć własną deklarację wystąpienia. Jeśli to wystąpienie jest inne niż twoje, musisz umieścić otokę nowego typu wokół swojego typu - co może być znaczącą refaktoryzacją kodu.
Yitz
@Matt: rzeczywiście, zaskakująco Scala dostaje to właśnie tam, gdzie Haskell nie! (z wyjątkiem oczywiście Scali brak pierwszej klasy składni dla maszyn typu, co jest jeszcze gorsze ...)
Erik Kaplun
44

Śmiało i powstrzymaj to ostrzeżenie!

Jesteś w dobrym towarzystwie. Conal robi to w „TypeCompose”. Robią to „chp-mtl” i „chp-transformers”, „control-monad-extra-mtl” i „control-monad-extra-monadsfd” itd.

btw prawdopodobnie już to wiesz, ale dla tych, którzy tego nie robią i natkną się na Twoje pytanie podczas wyszukiwania:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Edytować:

Uznaję problemy, które Yitz wymienił w swojej odpowiedzi, jako prawdziwe problemy. Jednak uważam, że nie traktuję osieroconych instancji również jako problemu i staram się wybrać „najmniejsze zło”, czyli roztropne korzystanie z instancji osieroconych.

W mojej krótkiej odpowiedzi użyłem tylko wykrzyknika, ponieważ z twojego pytania wynika, że ​​jesteś już dobrze świadomy problemów. W przeciwnym razie byłbym mniej entuzjastyczny :)

Trochę urozmaicenia, ale uważam, że to idealne rozwiązanie w idealnym świecie bez kompromisów:

Uważam, że problemy, o których wspomina Yitz (nie wiedząc, która instancja jest wybrana) można rozwiązać w „holistycznym” systemie programowania, w którym:

  • Nie edytujesz zwykłych plików tekstowych, ale raczej korzystasz z pomocy środowiska (na przykład uzupełnianie kodu tylko sugeruje rzeczy odpowiedniego typu itp.)
  • Język „niższego poziomu” nie ma specjalnej obsługi klas typów, a zamiast tego tabele funkcji są przekazywane jawnie
  • Ale środowisko programistyczne „wyższego poziomu” wyświetla kod w podobny sposób, jak Haskell jest teraz przedstawiany (zwykle nie zobaczysz przekazanych tabel funkcji) i wybiera jawne klasy typu dla Ciebie, gdy są oczywiste (np. przykład wszystkie przypadki Functora mają tylko jeden wybór), a gdy istnieje kilka przykładów (lista zip Applicative lub list-monad Applicative, First / Last / lift może Monoid) pozwala wybrać, której instancji użyć.
  • W każdym razie, nawet jeśli instancja została wybrana automatycznie, środowisko łatwo pozwala zobaczyć, która instancja została użyta, za pomocą łatwego interfejsu (hiperłącze lub interfejs najechania kursorem lub coś takiego)

Wracając ze świata fantasy (lub miejmy nadzieję, że z przyszłości), teraz: Zalecam unikanie instancji osieroconych, a jednocześnie ich używanie, gdy „naprawdę potrzebujesz”

yairchu
źródło
5
Tak, ale zapewne każde z tych zdarzeń jest błędem jakiejś kolejności. Przychodzą mi na myśl złe przypadki w kontrolnych-monad-wyjątek-mtl i monad-fd dla Albo. Byłoby mniej natrętne, gdyby każdy z tych modułów był zmuszony do definiowania własnych typów lub dostarczania opakowań typu newtype. Prawie każda sieroca instancja to ból głowy, który czeka, a jeśli nic innego nie będzie wymagało twojej stałej czujności, aby upewnić się, że jest importowany lub nieodpowiedni.
Edward KMETT
2
Dzięki. Myślę, że użyję ich w tej konkretnej sytuacji, ale dzięki Yitz teraz lepiej rozumiem, jakie problemy mogą powodować.
Dan Dyer
37

Instancje sieroce są uciążliwe, ale moim zdaniem czasami są potrzebne. Często łączę biblioteki, w których typ pochodzi z jednej biblioteki, a klasa pochodzi z innej. Oczywiście od autorów tych bibliotek nie można oczekiwać, że zapewnią instancje dla każdej możliwej kombinacji typów i klas. Muszę je więc zapewnić, więc są sierotami.

Pomysł, że należy opakować typ nowym typem, gdy trzeba podać instancję, jest pomysłem o wartości teoretycznej, ale w wielu przypadkach jest po prostu zbyt uciążliwy; to rodzaj pomysłu ludzi, którzy nie piszą kodu Haskella na życie. :)

Więc idź dalej i zapewnij osierocone instancje. Są nieszkodliwe.
Jeśli możesz zawiesić ghc z osieroconymi instancjami, jest to błąd i powinien zostać zgłoszony jako taki. (Błąd, który miał / ma ghc dotyczący niewykrywania wielu instancji, nie jest trudny do naprawienia).

Ale pamiętaj, że w przyszłości ktoś inny może dodać taką instancję, jak już masz, i możesz otrzymać błąd (czas kompilacji).

augustss
źródło
2
Dobrym przykładem jest (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)użycie QuickCheck.
Erik Kaplun
17

W tym przypadku uważam, że użycie instancji osieroconych jest w porządku. Ogólna zasada dla mnie jest taka - możesz zdefiniować instancję, jeśli "jesteś" właścicielem "typeklasy lub jeśli" posiadasz "typ danych (lub jakiś jego komponent - np. Instancja dla Może MyData też jest w porządku, przynajmniej czasami). W ramach tych ograniczeń miejsce, w którym zdecydujesz się umieścić instancję, jest Twoim własnym biznesem.

Jest jeszcze jeden wyjątek - jeśli nie jesteś właścicielem typeklasy ani typu danych, ale tworzysz plik binarny, a nie bibliotekę, to też jest w porządku.

sclv
źródło
5

(Wiem, że spóźniłem się na imprezę, ale to może być przydatne dla innych)

Możesz zachować osierocone instancje w swoim własnym module, a jeśli ktoś zaimportuje ten moduł, to specjalnie dlatego, że ich potrzebuje i może uniknąć importowania ich, jeśli powodują problemy.

Trystan Spangler
źródło
3

W związku z tym rozumiem położenie bibliotek WRT w obozie instancji anty-osieroconych, ale czy w przypadku celów wykonywalnych nie powinno być w porządku?

mxc
źródło
3
Jeśli chodzi o bycie niegrzecznym wobec innych, masz rację. Ale otwierasz się na potencjalne przyszłe problemy, jeśli ta sama instancja zostanie kiedykolwiek zdefiniowana w przyszłości gdzieś w twoim łańcuchu zależności. W takim przypadku to Ty decydujesz, czy warto zaryzykować.
Yitz
5
W prawie wszystkich przypadkach implementacji osieroconej instancji w pliku wykonywalnym, ma ona wypełnić lukę, którą chcesz , aby została już zdefiniowana. Jeśli więc instancja pojawia się powyżej, wynikający z tego błąd kompilacji jest tylko użytecznym sygnałem, który mówi, że możesz usunąć swoją deklarację instancji.
Ben,