Podczas próby debugowania problemu w moim programie (2 okręgi o równym promieniu są rysowane w różnych rozmiarach za pomocą Gloss *
), natknąłem się na dziwną sytuację. W moim pliku, który obsługuje obiekty, mam następującą definicję Player
:
type Coord = (Float,Float)
data Obj = Player { oPos :: Coord, oDims :: Coord }
aw moim głównym pliku, który importuje Objects.hs, mam następującą definicję:
startPlayer :: Obj
startPlayer = Player (0,0) 10
Stało się to przez to, że dodałem i zmieniłem pola dla gracza i zapomniałem zaktualizować startPlayer
po (jego wymiary zostały określone przez jedną liczbę reprezentującą promień, ale zmieniłem to na a, Coord
aby reprezentować (szerokość, wysokość); na wypadek, gdyby kiedykolwiek zrobiłem obiekt gracza nie jest kołem).
Niesamowite jest to, że powyższy kod kompiluje się i działa, mimo że drugie pole jest niewłaściwego typu.
Najpierw pomyślałem, że być może mam otwarte różne wersje plików, ale wszelkie zmiany w plikach zostały odzwierciedlone w skompilowanym programie.
Następnie pomyślałem, że może startPlayer
z jakiegoś powodu nie był używany. Komentowanie startPlayer
powoduje jednak błąd kompilatora, a co dziwniejsze, zmiana 10
in startPlayer
powoduje odpowiednią odpowiedź (zmienia rozmiar początkowy elementu Player
); znowu, mimo że jest niewłaściwego typu. Aby upewnić się, że poprawnie odczytuje definicję danych, wstawiłem do pliku literówkę i wystąpił błąd; więc patrzę na właściwy plik.
Próbowałem wklejanie fragmentów powyżej 2 do własnego pliku i go wypluł oczekiwany błąd, że drugie pole Player
w startPlayer
jest nieprawidłowy.
Co mogłoby na to pozwolić? Można by pomyśleć, że jest to dokładnie to, co powinno zapobiegać sprawdzaniu typów przez Haskella.
*
Odpowiedzią na mój pierwotny problem, polegającą na narysowaniu dwóch okręgów o rzekomo równym promieniu w różnych rozmiarach, było to, że jeden z promieni był w rzeczywistości ujemny.
Point
anewtype
lub użyj innych nazw operatorów alalinear
)Odpowiedzi:
Jedynym sposobem, w jaki mogłoby to się skompilować, jest istnienie
Num (Float,Float)
instancji. Nie zapewnia tego biblioteka standardowa, chociaż możliwe jest, że jedna z używanych bibliotek dodała ją z jakiegoś szalonego powodu. Spróbuj załadować swój projekt w ghci i sprawdź, czy10 :: (Float,Float)
działa, a następnie spróbuj:i Num
dowiedzieć się, skąd pochodzi instancja, a następnie krzycz na osobę, która ją zdefiniowała.Dodatek: nie ma możliwości wyłączenia instancji. Nie ma nawet sposobu, aby nie wyeksportować ich z modułu. Gdyby to było możliwe, doprowadziłoby to do jeszcze bardziej zagmatwanego kodu. Jedynym prawdziwym rozwiązaniem jest nie definiowanie takich instancji.
źródło
10 :: (Float, Float)
daje(10.0,10.0)
i:i Num
zawiera linięinstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’
(Point
jest aliasem Glossa coord). Poważnie? Dziękuję Ci. To uratowało mnie przed bezsenną nocą.Num
których ma to sens, na przykładAngle
typ danych, który ograniczaDouble
między-pi
ipi
, lub jeśli ktoś chciałby napisać typ danych reprezentowanie kwaternionów lub innego bardziej złożonego typu numerycznego ta funkcja jest bardzo wygodna. Podąża również za tymi samymi regułami, coString
/Text
/ByteString
, zezwalanie na te wystąpienia ma sens z punktu widzenia łatwości użycia, ale może być nadużywane, jak w tym przypadku.newtype
deklaracji dlaCoord
zamiast plikutype
.Num (Float, Float)
lub nawet(Floating a) => Num (a,a)
nie wymagałaby rozszerzenia, ale spowodowałaby takie samo zachowanie.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 instancjaNum (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 ifromInteger
problemDziwisz się, że kompilator akceptuje
10 :: Coord
, tj10 :: (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ć jakoInt
,Integer
,Float
, lubDouble
. Krotka liczb bez innego kontekstu nie wygląda na liczbę w taki sposób, w jaki te cztery typy są liczbami. Nie rozmawiamy oComplex
.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 jakofromInteger 10
, który ma typNum a => a
.10
Można więc wywnioskować, że każdy typ, dla którego zostałaNum
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 jakNum a => Num (a, a)
lubNum (Float, Float)
. Nie ma takiej instancji w programiePrelude
, 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 instancjagloss
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 pisanieCoord
jest po prostu skrótem(Float, Float)
. Podobnie wGraphics.Gloss.Data.Point
,Point
oznacza(Float, Float)
. Innymi słowy,Coord
andgloss
„sPoint
dosłownie równoważne.Więc kiedy
gloss
opiekunowie zdecydowali się napisaćinstance Num Point where ...
, również uczynili twójCoord
typ instancjąNum
. To jest równoważneinstance Num (Float, Float) where ...
lubinstance Num Coord where ...
.(Domyślnie Haskell nie pozwala, aby synonimy typów były instancjami klas.
gloss
Autorzy musieli włączyć parę rozszerzeń językowychTypeSynonymInstances
iFlexibleInstances
napisać instancję).Po drugie, jest to zaskakujące, ponieważ jest to instancja osierocona , tj. Deklaracja instancji, w
instance C A
której obaC
iA
są zdefiniowane w innych modułach. Tutaj jest to szczególnie podstępne, ponieważ każda z zaangażowanych części, tj.Num
,(,)
IFloat
, pochodzi zPrelude
i prawdopodobnie będzie objęta zakresem wszędzie.Twoje oczekiwanie jest
Num
zdefiniowane wPrelude
, krotki iFloat
są zdefiniowane wPrelude
, więc wszystko, co dotyczy tych trzech rzeczy, jest zdefiniowane wPrelude
. 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, zdefiniowaniefromInteger = 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 Kmettlinear
(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 dodalitransformers
. 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
zamiasttype
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.
źródło