Sprawdzanie typów przez Haskella jest rozsądne. Problem w tym, że autorzy biblioteki, z której korzystasz, zrobili coś ... mniej rozsądnego.
Krótka odpowiedź brzmi: tak, 10 :: (Float, Float)
jest całkowicie poprawna, jeśli istnieje instancja Num (Float, Float)
. Nie ma w tym nic „bardzo złego” z punktu widzenia kompilatora lub języka. Po prostu nie zgadza się to z naszą intuicją dotyczącą literałów numerycznych. Ponieważ jesteś przyzwyczajony do tego, że system typów wyłapuje rodzaj popełnionego błędu, słusznie jesteś zaskoczony i rozczarowany!
Num
przypadki i fromInteger
problem
Dziwisz się, że kompilator akceptuje 10 :: Coord
, tj 10 :: (Float, Float)
. Rozsądnie jest założyć, że literały numeryczne, takie jak, 10
będą miały typy „numeryczne”. Po wyjęciu z pudełka, literały liczbowe można interpretować jako Int
, Integer
, Float
, lub Double
. Krotka liczb bez innego kontekstu nie wygląda na liczbę w taki sposób, w jaki te cztery typy są liczbami. Nie rozmawiamy o Complex
.
Na szczęście lub niestety, Haskell jest językiem bardzo elastycznym. Standard określa, że literał liczby całkowitej, taki jak, 10
będzie interpretowany jako fromInteger 10
, który ma typ Num a => a
. 10
Można więc wywnioskować, że każdy typ, dla którego została Num
napisana instancja. Wyjaśnię to nieco bardziej szczegółowo w innej odpowiedzi .
Kiedy więc opublikowałeś swoje pytanie, doświadczony Haskeller natychmiast zauważył, że 10 :: (Float, Float)
aby zostać zaakceptowanym, musi istnieć instancja taka jak Num a => Num (a, a)
lub Num (Float, Float)
. Nie ma takiej instancji w programie Prelude
, więc musiała zostać zdefiniowana gdzie indziej. Używając :i Num
, szybko zorientowałeś się, skąd pochodzi: gloss
paczka.
Wpisz synonimy i przypadki osierocone
Ale poczekaj chwilę. W gloss
tym przykładzie nie używasz żadnych typów; dlaczego ta instancja gloss
wpłynęła na Ciebie? Odpowiedź jest w dwóch krokach.
Po pierwsze, synonim typu wprowadzony za pomocą słowa kluczowego type
nie tworzy nowego typu . W twoim module pisanie Coord
jest po prostu skrótem (Float, Float)
. Podobnie w Graphics.Gloss.Data.Point
, Point
oznacza (Float, Float)
. Innymi słowy, Coord
and gloss
„s Point
dosłownie równoważne.
Więc kiedy gloss
opiekunowie zdecydowali się napisać instance Num Point where ...
, również uczynili twój Coord
typ instancją Num
. To jest równoważne instance Num (Float, Float) where ...
lub instance Num Coord where ...
.
(Domyślnie Haskell nie pozwala, aby synonimy typów były instancjami klas. gloss
Autorzy musieli włączyć parę rozszerzeń językowych TypeSynonymInstances
i FlexibleInstances
napisać instancję).
Po drugie, jest to zaskakujące, ponieważ jest to instancja osierocona , tj. Deklaracja instancji, w instance C A
której oba C
i A
są zdefiniowane w innych modułach. Tutaj jest to szczególnie podstępne, ponieważ każda z zaangażowanych części, tj. Num
, (,)
I Float
, pochodzi z Prelude
i prawdopodobnie będzie objęta zakresem wszędzie.
Twoje oczekiwanie jest Num
zdefiniowane w Prelude
, krotki i Float
są zdefiniowane w Prelude
, więc wszystko, co dotyczy tych trzech rzeczy, jest zdefiniowane w Prelude
. Dlaczego import zupełnie innego modułu miałby cokolwiek zmienić? Idealnie by tak nie było, ale przypadki osierocone łamią tę intuicję.
(Zwróć uwagę, że GHC ostrzega przed osieroconymi instancjami - autorzy gloss
szczególnie zignorowali to ostrzeżenie. Powinno to podnieść czerwoną flagę i przynajmniej ostrzeżenie w dokumentacji).
Instancje klas są globalne i nie można ich ukryć
Ponadto instancje klas są globalne : każdy przypadek określony w dowolnym module, który jest przechodni importowanego z Twojego modułu będzie w kontekście i dostępny do typechecker robiąc rozdzielczość instancji. To sprawia, że rozumowanie globalne jest wygodne, ponieważ możemy (zwykle) założyć, że funkcja klasy taka jak (+)
zawsze będzie taka sama dla danego typu. Jednak oznacza to również, że lokalne decyzje mają skutki globalne; zdefiniowanie instancji klasy nieodwołalnie zmienia kontekst dalszego kodu, bez możliwości zamaskowania lub ukrycia go za granicami modułu.
Nie można używać list importu, aby uniknąć importowania instancji . Podobnie nie można uniknąć eksportowania wystąpień ze zdefiniowanych modułów.
Jest to problematyczny i często dyskutowany obszar projektowania języka Haskell. W tym wątku reddit znajduje się fascynująca dyskusja na temat powiązanych problemów . Zobacz na przykład komentarz Edwarda Kmetta na temat zezwolenia na kontrolę widoczności instancji: „W zasadzie odrzucasz poprawność prawie całego kodu, który napisałem”.
(Nawiasem mówiąc, jak wykazała ta odpowiedź , można pod pewnymi względami złamać założenie instancji globalnej, używając instancji osieroconych!)
Co robić - dla osób wdrażających biblioteki
Pomyśl dwa razy przed wdrożeniem Num
. Nie możesz obejść fromInteger
problemu - nie, zdefiniowanie fromInteger = error "not implemented"
go nie poprawia. Czy Twoi użytkownicy będą zdezorientowani lub zaskoczeni - lub, co gorsza, nigdy nie zauważą - jeśli ich literały liczb całkowitych zostaną przypadkowo wywnioskowane jako typ, którego instancję tworzysz? Czy zapewnienie (*)
i (+)
to krytyczne - szczególnie jeśli musisz je zhakować?
Rozważ użycie alternatywnych operatorów arytmetycznych zdefiniowanych w bibliotece, takich jak Conal Elliott vector-space
(dla typów rodzajów *
) lub Edward Kmett linear
(dla typów rodzajów * -> *
). To właśnie robię sam.
Użyj -Wall
. Nie implementuj osieroconych instancji i nie wyłączaj ostrzeżenia o osieroconych instancjach.
Alternatywnie, podążaj za przykładem linear
i wieloma innymi dobrze wychowanymi bibliotekami i udostępniaj osierocone instancje w oddzielnym module kończącym się na .OrphanInstances
lub .Instances
. I nie importuj tego modułu z żadnego innego modułu . Następnie użytkownicy mogą jawnie importować sieroty, jeśli chcą.
Jeśli stwierdzisz, że definiujesz sieroty, rozważ poproszenie zewnętrznych opiekunów o ich implementację, jeśli to możliwe i właściwe. Często pisałem instancję osieroconą Show a => Show (Identity a)
, dopóki jej nie dodali transformers
. Mogłem nawet zgłosić błąd w tej sprawie; Nie pamiętam.
Co robić - dla konsumentów bibliotecznych
Nie masz wielu opcji. Dotrzyj - uprzejmie i konstruktywnie! - do opiekunów biblioteki. Wskaż im to pytanie. Mogli mieć jakiś szczególny powód, by napisać problematyczną sierotę, albo po prostu nie zdają sobie z tego sprawy.
Szerzej: bądź świadomy tej możliwości. Jest to jeden z niewielu obszarów Haskell, w którym istnieją prawdziwe globalne skutki; musiałbyś sprawdzić, czy każdy moduł, który importujesz, i każdy moduł, który te moduły importują, nie implementuje instancji osieroconych. Adnotacje typu mogą czasami ostrzegać o problemach i oczywiście możesz użyć :i
w GHCi, aby to sprawdzić.
Zdefiniuj własne newtype
zamiast type
synonimów, jeśli jest to wystarczająco ważne. Możesz być całkiem pewien, że nikt z nimi nie zadziera.
Jeśli często masz problemy z pobieraniem z biblioteki open source, możesz oczywiście stworzyć własną wersję biblioteki, ale konserwacja może szybko stać się bólem głowy.