Dlaczego Bounded nie jest podklasą Enum w Haskell

9

Wygląda na to, że każda instancja Bounded powinna mieć rozsądną implementację Enum. Nie mogę osobiście wymyślić kontrprzykładu, chociaż jeśli ktoś wymyśli taki, który nie jest patologiczny, zrozumiem, dlaczego tak nie jest.

Z robienia :ina dwóch typach klas wydaje się, że jedynym wyjątkiem obecnie w standardowej bibliotece są krotki, które są ograniczone, ale nie wyliczają. Jednak każda krotka Bounded musi być również wyliczalna w rozsądny sposób, po prostu zwiększając ostatni element, a następnie zawijając, gdy dojdzie do maxBound.

Ta zmiana prawdopodobnie wymagałaby również dodania predBi / nextBlub czegoś takiego do Bounded w celu bezpiecznego / zapętlonego sposobu przechodzenia przez wartości Enum. W takim przypadku toEnum 0 :: (...)byłoby równe(toEnum 0, toEnum 0, ...) :: (...)

średnik
źródło
3
Naprawdę nie mogę odpowiedzieć na to autorytatywnie, ale rozważmy zakres wszystkich liczb rzeczywistych od 0 do 1. Ma wyraźne dolne i górne granice, ale ma niezliczoną liczbę członków.
Doval
@Doval to uczciwa kwestia. Jednak to samo można powiedzieć o wszystkich liczbach rzeczywistych w ogóle (niezliczonych członkach nieskończonych), ale Double/ Floati wszystkie podobne typy i Enumtak implementują , po prostu tworzą succ = (+ 1)i fromEnum = truncate. Sposób Haskella ma sens z praktycznego punktu widzenia, ponieważ w przeciwnym razie [0, 0,5 ..] i podobne nie działałyby, więc wydaje się, że Haskell nie martwi się o policzalność, jeśli chodzi o Enums.
średnik
1
I nie był świadomy, że succjest (+1). To dziwne, ponieważ Doublei Floatnie mają nieskończonej precyzji, a zatem policzalne - succmożna by je zdefiniować jako +1 ULP .
Doval
2
@Doval Myślę, że powodem tego był fakt, że zespół podstawowy Haskell chciał, aby [1 ..] oznaczać to samo w Doubles, co oznacza w Ints.
średnik
@semicolon podwaja się i liczby zmiennoprzecinkowe nie są liczbami rzeczywistymi (np. nie mogą przechowywać PI w podwójnej wartości bez utraty pewnej precyzji), więc są policzalne
jk.

Odpowiedzi:

8

Jeden praktyczny przykład, który mi się podoba, pochodzi ze świata języków programowania: zestaw typów w systemie OO jest ograniczony i dyskretny, ale nie można go wyliczyć i częściowo uporządkować, ale nie uporządkować całkowicie.

Przedmiotowe częściowe uporządkowanie jest relacją podtypu <:. Górna granica byłaby wówczas rodzajem górnym (które wywołuje C # objecti wywołania Scali Any), a dolna granica byłaby typem dolnym (Scala Nothing; C # / Java nie ma odpowiednika, o którym można mówić).

Jednak nie ma sposobu na wyliczenie wszystkich typów w systemie typów, więc nie można napisać an instance Enum Type. Powinno to być jasne: użytkownicy mogą pisać własne typy, więc nie ma sposobu, aby wiedzieć, co będą z góry. Możesz wyliczyć wszystkie typy w dowolnym programie, ale nie w całym systemie.

Podobnie (zgodnie z pewną rozsądną definicją podtypu) <:jest zwrotny, przechodni i antysymetryczny, ale nie całkowity . Istnieją pary typów, które nie są powiązane <:. ( Cati Dogoba są podtypami Animal, ale żaden z nich nie jest podtypem drugiego).


Załóżmy, że piszemy kompilator dla prostego języka OO. Oto reprezentacja typów w naszym systemie:

data Type = Bottom | Class { name :: String, parent :: Type } | Top

I definicja relacji podtypu:

(<:) :: Type -> Type -> Bool
Bottom <: _ = True
Class _ _ <: Bottom = False
Class n t <: s@(Class m _)
    | n == m = True  -- you can't have different classes with the same name in this hypothetical language
    | otherwise = t <: s  -- try to find s in the parents of this class
Class _ _ <: Top = True
Top <: Top = True
Top <: _ = False

Daje nam to również relację nadrzędności.

(>:) :: Type -> Type -> Bool
t >: s = s <: t

Możesz także znaleźć najmniejszą górną granicę dwóch typów,

lub :: Type -> Type -> Type
lub Bottom s = s
lub t Bottom = t
lub t@(Class _ p) s@(Class _ q) =
    | t >: s = t
    | t <: s = s
    | p >: s = p
    | t <: q = q
    | otherwise = lub p q
lub Top _ = Top
lub _ Top = Top

Ćwiczenie: pokaż, że Typetworzy pełny kompletny zbiór na dwa sposoby, pod <:i pod >:.

Benjamin Hodgson
źródło
Wielkie dzieki! To całkowicie odpowiada na moje pytanie, a także odpowiada na moje dalsze pytanie dotyczące zamówienia. Czy Eq miałby podobne problemy? Tam, gdzie typ niezgodny może mieć maxBound lub minBound. Czy w takim przypadku pies == Pies powinien po prostu zwrócić wartość fałszywą, ponieważ są to różne klasy, czy też byłoby to nierozstrzygalne z powodu pozycji drzewa nie stawiającej ani powyżej, ani poniżej drugiej?
średnik
Kolejność oznacza równość - wystarczy zdefiniować x == y = x <= y && y <= x. Gdybym projektował Posetklasę, miałbym class Eq a => Poset a. Szybkie Google potwierdza, że inne osoby miały ten sam pomysł .
Benjamin Hodgson
Przepraszam, moje pytanie było dwuznaczne. Miałem na myśli to, czy Bounded sugerował Eq, nawet jeśli nie sugeruje to Ord.
średnik
@semicolon Znowu nie ma związku między dwiema klasami. Zastanów się data Bound a = Min | Val a | Max, które wzmacnia typ az +∞i -∞elementy. Z konstrukcji Bound amożna zawsze zrobić przykład, Boundedale byłoby to możliwe tylko wtedy, gdy typem podstawowym ajest
Benjamin Hodgson
w porządku, w porządku. Myślę, że jednym z przykładów mogą być funkcje, które pobierają i zwracają wartości typu Double, gdzie const (1/0)jest maxBoundi const (negate 1/0)jest, minBoundale są \x -> 1 - xi \x -> x - 1są nieporównywalne.
średnik
4

Wynika to z faktu, że operacje są niezależne, więc powiązanie ich z relacją podklasy tak naprawdę niczego nie kupi. Załóżmy, że chcesz utworzyć niestandardowy typ, który został zaimplementowany Bounded, być może Doublesograniczony między wartością maksymalną a minimalną, ale żadna z Enumoperacji nie była konieczna . Gdyby Boundedbyła podklasą, i tak musiałbyś zaimplementować wszystkie Enumfunkcje, żeby ją skompilować.

Tak naprawdę nie ma znaczenia, czy istnieje rozsądna implementacja Enumlub jakakolwiek inna liczba klas. Jeśli tak naprawdę go nie potrzebujesz, nie powinieneś być zmuszany do jego wdrożenia.

Porównaj to z powiedzmy Ordi Eq. Tam Ordoperacje są zależne od Eqtych, więc sensowne jest wymaganie podklasy, aby uniknąć powielania i zapewnić spójność.

Karl Bielefeldt
źródło
1
W takich przypadkach jest to część definicji. Wszystkie monady są z definicji aplikacjami i funktorami, więc nie można wypełnić „kontraktu” monady bez spełnienia pozostałych. Nie jestem wystarczająco zaznajomiony z matematyką, aby wiedzieć, czy jest to podstawowy związek, czy narzucona definicja, ale tak czy inaczej, utknęliśmy z tym teraz.
Karl Bielefeldt
6
@semicolon DokumentacjaBounded mówi, że „ Zamówienie nie jest nadklasą Bounded, ponieważ typy, które nie są całkowicie uporządkowane, mogą mieć także górne i dolne granice”.
Benjamin Hodgson
1
@BenjaminHodgson Nawet nie myślałem o częściowo zamówionych typach. +1 za niepatologiczny przykład i za cytowanie dokumentacji.
Doval
1
@semicolon Przykładem częściowego uporządkowania ze świata komputerów może być subtypowanie w językach OO. Pisanie <:dla jest podtypem , ∀ T S. T <: S ∨ S <: Tnie ma (np int !<: bool ∧ bool !<: int.). Prawdopodobnie byś na to wpadł, gdybyś pisał kompilator.
Benjamin Hodgson
1
@BenjaminHodgson ah ok. Na przykład, jeśli A jest nadklasą B i C, a D jest podklasą B i C, to B i C są nieporównywalne, ale A i D wynoszą maks./min?
średnikiem