Tutaj „wartości”, „typy” i „rodzaje” mają znaczenie formalne, więc biorąc pod uwagę ich powszechne użycie w języku angielskim lub analogie do klasyfikacji samochodów, dotrzesz tylko do tej pory.
Moja odpowiedź dotyczy formalnego znaczenia tych terminów w kontekście Haskell; znaczenia te są oparte na (choć nie są tak naprawdę identyczne) znaczeniach używanych w matematycznej / CS „teorii typów”. Nie będzie to więc bardzo dobra odpowiedź „informatyczna”, ale powinna służyć jako całkiem dobra odpowiedź Haskella.
W języku Haskell (i innych językach) pomocne jest przypisanie typu do wyrażenia programu opisującego klasę wartości, jakie wyrażenie może posiadać. Zakładam, że widziałeś wystarczająco dużo przykładów, aby zrozumieć, dlaczego warto wiedzieć, że w wyrażeniu sqrt (a**2 + b**2)
zmienne a
i b
zawsze będą wartościami typu, Double
a nie, powiedzmy, String
i Bool
odpowiednio. Zasadniczo posiadanie typów pomaga nam pisać wyrażenia / programy, które będą działać poprawnie w szerokim zakresie wartości .
Teraz możesz nie zdawać sobie sprawy, że typy Haskell, takie jak te, które pojawiają się w podpisach typów:
fmap :: Functor f => (a -> b) -> f a -> f b
tak naprawdę są napisane w podrzędnym języku Haskell. Tekst programu Functor f => (a -> b) -> f a -> f b
jest - dosłownie - wyrażeniem typu napisanym w tym języku podrzędnym. Podjęzykiem obejmuje operatorów (np ->
jest słuszną asocjacyjne operator infix w tym języku), zmienne (np f
, a
i b
) oraz „Aplikacja” jednego wyrazu do drugiego typu (na przykład f a
jest f
stosowane do a
).
Czy wspomniałem, jak w wielu językach pomocne było przypisywanie typów do wyrażeń programu w celu opisania klas wartości wyrażeń? Cóż, w tym podrzędnym języku typów wyrażenia oceniają na typy (a nie na wartości ), dlatego pomocne jest przypisywanie rodzajów do wyrażeń typu w celu opisania klas typów , które mogą reprezentować. Zasadniczo posiadanie rodzajów pomaga nam pisać wyrażenia typów, które będą działać poprawnie w szerokim zakresie typów .
Tak więc wartości są typu jako typy są do rodzajów i typów pomóc nam pisać wartość programów -level natomiast rodzaje pomóc nam pisać typu programów -level.
Jak wyglądają te rodzaje ? Rozważmy podpis typu:
id :: a -> a
Jeśli wyrażenie typu a -> a
jest ważne, co niby od rodzaju powinniśmy pozwolić zmienna a
być? Cóż, wyrażenia typu:
Int -> Int
Bool -> Bool
wyglądać prawidłowy, więc typów Int
i Bool
są oczywiście z prawej rodzaju . Ale nawet bardziej skomplikowane typy, takie jak:
[Double] -> [Double]
Maybe [(Double,Int)] -> Maybe [(Double,Int)]
wyglądać poprawnie. W rzeczywistości, ponieważ powinniśmy być w stanie wywoływać id
funkcje, nawet:
(a -> a) -> (a -> a)
wygląda w porządku. Więc Int
, Bool
, [Double]
, Maybe [(Double,Int)]
, a a -> a
wszystkie wyglądają jak typy z prawej rodzaju .
Innymi słowy, wygląda na to, że istnieje tylko jeden rodzaj , nazwijmy go *
jak wieloznaczny uniks, a każdy typ ma ten sam rodzaj *
, koniec historii.
Dobrze?
Cóż, niezupełnie. Okazuje się, że Maybe
samo w sobie jest tak samo poprawnym wyrażeniem typu jak Maybe Int
(w bardzo podobny sposób sqrt
, samo w sobie, jest tak samo poprawnym wyrażeniem wartości jak sqrt 25
). Jednak następujące wyrażenie typu jest nieprawidłowe:
Maybe -> Maybe
Ponieważ, gdy Maybe
jest wyrażeniem typu, nie stanowią swego rodzaju z rodzaju , które mogą mieć wartości. Tak, to w jaki sposób należy zdefiniować *
- to rodzaj z typów , które mają wartości; zawiera „Complete” typy jak Double
i Maybe [(Double,Int)]
ale nie obejmuje niekompletne, typy bezwartościowe podoba Either String
. Dla uproszczenia nazywam te kompletne rodzaje *
„konkretnymi typami”, chociaż terminologia ta nie jest uniwersalna, a „konkretne typy” mogą oznaczać coś zupełnie innego niż, powiedzmy, programista C ++.
Teraz w wyrażeniu typu a -> a
, o ile typ a
ma rodzaj *
(rodzaj konkretnych typów), wynik wyrażenia typu równieża -> a
będzie miał rodzaj (tj. Rodzaj konkretnych typów). *
Więc co niby od typu jest Maybe
? Cóż, Maybe
można go zastosować do rodzaju betonu, aby uzyskać inny rodzaj betonu. Tak, Maybe
wygląda jak mały jak rodzaj funkcji poziomu, że trwa typu o rodzaju *
i zwraca typ z rodzaju *
. Gdybyśmy mieli funkcję poziomu wartość, która odbyła się wartość od rodzaju Int
i zwracana jest wartość o rodzaju Int
, że damy mu rodzaj podpisu Int -> Int
, więc przez analogię powinniśmy dać Maybe
się rodzaj podpisu * -> *
. GHCi zgadza się:
> :kind Maybe
Maybe :: * -> *
Wracam do:
fmap :: Functor f => (a -> b) -> f a -> f b
W podpisie tego typu zmienna f
ma rodzaj * -> *
i zmienne a
oraz b
ma rodzaj *
; wbudowany operator ->
ma rodzaj * -> * -> *
(pobiera rodzaj *
po lewej i jeden po prawej i zwraca również rodzaj *
). Na podstawie tego i zasad wnioskowania rodzaju można wywnioskować, że a -> b
jest to poprawny typ z rodzajem *
, f a
a f b
także są poprawne typy z rodzajem *
i (a -> b) -> f a -> f b
jest to poprawny typ rodzaju *
.
Innymi słowy, kompilator może „sprawdzić rodzaj” wyrażenia typu, (a -> b) -> f a -> f b
aby sprawdzić, czy jest poprawny dla zmiennych typu właściwego rodzaju, w ten sam sposób, w jaki „sprawdza typ”, sqrt (a**2 + b**2)
aby sprawdzić, czy jest poprawny dla zmiennych właściwego typu.
Powodem używania osobnych terminów „typy” i „rodzaje” (tj. Nie mówienie o „typach typów”) jest głównie po to, aby uniknąć zamieszania. Powyższe rodzaje wyglądają bardzo inaczej niż typy i przynajmniej na początku wydają się zachowywać zupełnie inaczej. (Na przykład, zajmuje trochę czasu, aby objąć głowę myślą, że każdy „normalny” typ ma ten sam rodzaj, *
a taki nie a -> b
jest .)*
* -> *
Niektóre z nich są również historyczne. W miarę ewolucji GHC Haskell różnice między wartościami, typami i rodzajami zaczęły się zacierać. W dzisiejszych czasach wartości można „promować” na typy, a typy i rodzaje są naprawdę takie same. Tak więc we współczesnym Haskell zarówno wartości mają typy, jak i typy ARE (prawie), a rodzaje typów to po prostu więcej typów.
@ user21820 poprosił o dodatkowe wyjaśnienie „typy i rodzaje są naprawdę takie same”. Żeby było trochę jaśniej, we współczesnym GHC Haskell (myślę, że od wersji 8.0.1) typy i rodzaje są traktowane jednakowo w większości kodu kompilatora. Kompilator dokłada starań, aby komunikaty o błędach rozróżniały „typy” i „rodzaje”, w zależności od tego, czy narzeka odpowiednio na typ wartości, czy typ.
Ponadto, jeśli żadne rozszerzenia nie są włączone, można je łatwo rozróżnić w języku powierzchniowym. Na przykład typy (wartości) mają reprezentację w składni (np. W sygnaturach typów), ale rodzaje (typów) są - jak sądzę - całkowicie niejawne i nie ma wyraźnej składni tam, gdzie się pojawiają.
Ale jeśli włączysz odpowiednie rozszerzenia, rozróżnienie między rodzajami i rodzajami w dużej mierze zniknie. Na przykład:
{-# LANGUAGE GADTs, TypeInType #-}
data Foo where
Bar :: Bool -> * -> Foo
Tutaj Bar
jest (zarówno wartość, jak i) typ. Jako typ, jego rodzajem jest Bool -> * -> Foo
, która jest funkcją na poziomie rodzaju , która przyjmuje rodzaj Bool
(który jest typem, ale także rodzaj) i rodzaj rodzaju *
i tworzy rodzaj Foo
. Więc:
type MyBar = Bar True Int
poprawnie sprawdza rodzaj.
Jak wyjaśnia @AndrejBauer w swojej odpowiedzi, brak rozróżnienia typów i rodzajów jest niebezpieczny - posiadanie typu / rodzaju, *
którego typ / rodzaj sam jest (co ma miejsce we współczesnym Haskell), prowadzi do paradoksów. Jednak system typów Haskell jest już pełen paradoksów z powodu braku rozwiązania, więc nie jest to uważane za coś wielkiego.