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!
Numprzypadki i fromIntegerproblem
Dziwisz się, że kompilator akceptuje 10 :: Coord, tj 10 :: (Float, Float). Rozsądnie jest założyć, że literały numeryczne, takie jak, 10bę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, 10będzie interpretowany jako fromInteger 10, który ma typ Num a => a. 10Można więc wywnioskować, że każdy typ, dla którego została Numnapisana 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: glosspaczka.
Wpisz synonimy i przypadki osierocone
Ale poczekaj chwilę. W glosstym przykładzie nie używasz żadnych typów; dlaczego ta instancja glosswpłynęła na Ciebie? Odpowiedź jest w dwóch krokach.
Po pierwsze, synonim typu wprowadzony za pomocą słowa kluczowego typenie tworzy nowego typu . W twoim module pisanie Coordjest po prostu skrótem (Float, Float). Podobnie w Graphics.Gloss.Data.Point, Pointoznacza (Float, Float). Innymi słowy, Coordand gloss„s Pointdosłownie równoważne.
Więc kiedy glossopiekunowie zdecydowali się napisać instance Num Point where ..., również uczynili twój Coordtyp 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. glossAutorzy musieli włączyć parę rozszerzeń językowych TypeSynonymInstancesi FlexibleInstancesnapisać instancję).
Po drugie, jest to zaskakujące, ponieważ jest to instancja osierocona , tj. Deklaracja instancji, w instance C Aktórej oba Ci Asą 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 Preludei prawdopodobnie będzie objęta zakresem wszędzie.
Twoje oczekiwanie jest Numzdefiniowane w Prelude, krotki i Floatsą 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 glossszczegó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ść fromIntegerproblemu - 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 lineari wieloma innymi dobrze wychowanymi bibliotekami i udostępniaj osierocone instancje w oddzielnym module kończącym się na .OrphanInstanceslub .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ć :iw GHCi, aby to sprawdzić.
Zdefiniuj własne newtypezamiast typesynonimó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.