Haskell Language
Klasy typów
Szukaj…
Wprowadzenie
Klasy typów w Haskell są sposobem na zdefiniowanie zachowania związanego z typem oddzielnie od definicji tego typu. Podczas gdy, powiedzmy, w Javie, zdefiniujesz zachowanie jako część definicji typu - tj. W interfejsie, klasie abstrakcyjnej lub konkretnej - Haskell zachowuje te dwie rzeczy osobno.
W pakiecie base
Haskell zdefiniowano już kilka typów czcionek. Zależność między nimi zilustrowano w sekcji Uwagi poniżej.
Uwagi
Poniższy schemat zaczerpnięty z artykułu Typeclassopedia pokazuje związek między różnymi typami klas w Haskell.
Może i klasa Functor
W Haskell typy danych mogą mieć argumenty podobnie jak funkcje. Weźmy na przykład typ Maybe
.
Maybe
jest to bardzo przydatny typ, który pozwala nam przedstawić ideę niepowodzenia lub jego możliwość. Innymi słowy, jeśli istnieje możliwość, że obliczenia nie powiodą się, używamy tam typu Maybe
. Maybe
działa jak opakowanie dla innych typów, dając im dodatkową funkcjonalność.
Jego rzeczywista deklaracja jest dość prosta.
Maybe a = Just a | Nothing
Mówi to, że Maybe
występuje w dwóch formach: Just
, który reprezentuje sukces, i Nothing
, który reprezentuje porażkę. Just
wziąć jeden argument, który określa typ Maybe
, i Nothing
bierze żadnego. Na przykład wartość Just "foo"
będzie miała typ Maybe String
, który jest typem łańcucha zapakowanym z dodatkową funkcją Maybe
. Wartość Nothing
ma typu Maybe a
gdzie a
może być dowolnego typu.
Pomysł zawijania typów w celu zapewnienia im dodatkowej funkcjonalności jest bardzo przydatny i ma zastosowanie nie tylko do Maybe
. Inne przykłady obejmują Either
, IO
i typy list, z których każdy zapewnia inną funkcjonalność. Istnieją jednak pewne działania i zdolności, które są wspólne dla wszystkich tych rodzajów opakowań. Najważniejszą z nich jest możliwość modyfikacji wartości kapsułkowanej.
Często uważa się tego rodzaju typy za pola, w których mogą znajdować się wartości. Różne pola przechowują różne wartości i wykonują różne czynności, ale żadne z nich nie jest przydatne bez możliwości uzyskania dostępu do zawartości.
Aby zawrzeć ten pomysł, Haskell jest dostarczany ze standardową czcionką o nazwie Functor
. Jest zdefiniowany w następujący sposób.
class Functor f where
fmap :: (a -> b) -> f a -> f b
Jak widać, klasa ma jedną funkcję, fmap
, fmap
z dwóch argumentów. Pierwszy argument jest funkcją jednego typu, a
, drugiego, b
. Drugi argument to funktor (typ opakowania) zawierający wartość typu a
. Zwraca funktor (typ opakowania) zawierający wartość typu b
.
Mówiąc fmap
, fmap
przyjmuje funkcję i stosuje się do wartości wewnątrz funktora. Jest to jedyna funkcja niezbędna, aby typ był członkiem klasy Functor
, ale jest to niezwykle przydatne. Funkcje działające na funktorach, które mają bardziej szczegółowe zastosowania, można znaleźć w typach klas Applicative
i Monad
.
Dziedziczenie klasy typu: klasa typu Ord
Haskell popiera pojęcie rozszerzenia klasy. Na przykład klasa Ord
dziedziczy wszystkie operacje w Eq
, ale dodatkowo ma funkcję compare
która zwraca Ordering
między wartościami. Ord
mogą również zawierać typowe operatorów celu porównania, jak również min
sposobu i max
metody.
Notacja =>
ma takie samo znaczenie jak w sygnaturze funkcji i wymaga wpisania a
aby zaimplementować Eq
, aby zaimplementować Ord
.
data Ordering = EQ | LT | GT
class Eq a => Ord a where
compare :: Ord a => a -> a -> Ordering
(<) :: Ord a => a -> a -> Bool
(<=) :: Ord a => a -> a -> Bool
(>) :: Ord a => a -> a -> Bool
(>=) :: Ord a => a -> a -> Bool
min :: Ord a => a -> a -> a
max :: Ord a => a -> a -> a
Wszystkie poniższe metody compare
można z niego wyprowadzić na wiele sposobów:
x < y = compare x y == LT
x <= y = x < y || x == y -- Note the use of (==) inherited from Eq
x > y = not (x <= y)
x >= y = not (x < y)
min x y = case compare x y of
EQ -> x
LT -> x
GT -> y
max x y = case compare x y of
EQ -> x
LT -> y
GT -> x
Klasy typów, które same rozszerzają Ord
muszą implementować przynajmniej metodę compare
lub samą metodę (<=)
, która tworzy ukierunkowaną sieć dziedziczenia.
Równ
Wszystkie podstawowe typy danych (takie jak Int
, String
, Eq a => [a]
) z Prelude, z wyjątkiem funkcji i IO
mają instancje Eq
. Jeśli typ tworzy Eq
, oznacza to, że wiemy, jak porównać dwie wartości pod względem wartości lub równości strukturalnej .
> 3 == 2
False
> 3 == 3
True
Wymagane metody
-
(==) :: Eq a => a -> a -> Boolean
lub(/=) :: Eq a => a -> a -> Boolean
(jeśli tylko jeden jest zaimplementowany, drugi domyślnie neguje zdefiniowany)
Definiuje
-
(==) :: Eq a => a -> a -> Boolean
-
(/=) :: Eq a => a -> a -> Boolean
Bezpośrednie nadklasy
Żaden
Godne uwagi podklasy
Ord
Typy tworzące Ord
obejmują, np. Int
, String
i [a]
(dla typów, a
których występuje Ord a
instancja). Jeśli typ tworzy instancję Ord
, oznacza to, że znamy „naturalne” uporządkowanie wartości tego typu. Zauważ, że często istnieje wiele możliwych wyborów „naturalnego” uporządkowania typu i Ord
zmusza nas do faworyzowania jednego z nich.
Ord
udostępnia standardowe operatory (<=)
, (<)
, (>)
, (>=)
, ale co ciekawe, definiuje je wszystkie za pomocą niestandardowego typu danych algebraicznych
data Ordering = LT | EQ | GT
compare :: Ord a => a -> a -> Ordering
Wymagane metody
-
compare :: Ord a => a -> a -> Ordering
lub(<=) :: Ord a => a -> a -> Boolean
(domyślna metodacompare
standardu używa(<=)
w swojej implementacji)
Definiuje
-
compare :: Ord a => a -> a -> Ordering
-
(<=) :: Ord a => a -> a -> Boolean
-
(<) :: Ord a => a -> a -> Boolean
-
(>=) :: Ord a => a -> a -> Boolean
-
(>) :: Ord a => a -> a -> Boolean
-
min :: Ord a => a -> a -> a
-
max :: Ord a => a -> a -> a
Bezpośrednie nadklasy
Monoid
Typy tworzące Monoid
obejmują między innymi listy, liczby i funkcje z zwracanymi wartościami Monoid
. Aby utworzyć instancję Monoid
typ musi obsługiwać asocjacyjną operację binarną ( mappend
lub (<>)
), która łączy jego wartości, i ma specjalną wartość „zero” ( mempty
), tak że połączenie z nią wartości nie zmienia tej wartości:
mempty <> x == x
x <> mempty == x
x <> (y <> z) == (x <> y) <> z
Intuicyjnie typy Monoid
są „podobne do list”, ponieważ obsługują łączenie wartości razem. Alternatywnie, typy Monoid
można traktować jako sekwencje wartości, dla których zależy nam na porządku, ale nie na grupowaniu. Na przykład, drzewo binarne jest Monoid
, ale za pomocą operacji Monoid
nie możemy obserwować jego rozgałęzionej struktury, a jedynie przechodzenie przez jego wartości (patrz Foldable
i Traversable
).
Wymagane metody
-
mempty :: Monoid m => m
-
mappend :: Monoid m => m -> m -> m
Bezpośrednie nadklasy
Żaden
Num
Najbardziej ogólna klasa dla typów liczb, a dokładniej dla pierścieni , tzn. Liczb, które można dodawać, odejmować i mnożyć w zwykłym znaczeniu, ale niekoniecznie dzielą się.
Ta klasa zawiera zarówno typy całkowe ( Int
, Integer
, Word32
itp.), Jak i typy ułamkowe ( Double
, Rational
, również liczby zespolone itp.). W przypadku typów skończonych semantykę rozumie się ogólnie jako arytmetykę modułową , tj. Z przepełnieniem i niedopełnieniem † .
Zauważ, że reguły dla klas numerycznych są znacznie mniej ściśle przestrzegane niż prawa monady lub prawa monoidów lub te dla porównania równości . W szczególności liczby zmiennoprzecinkowe są zasadniczo zgodne z prawami tylko w przybliżeniu.
Metody
fromInteger :: Num a => Integer -> a
. przekonwertować liczbę całkowitą na ogólny typ liczb (w razie potrzeby zawijając wokół zakresu). Literały liczbowe Haskella można rozumieć jako monomorficzny literałInteger
z ogólną konwersją wokół niego, dzięki czemu można używać literału5
zarówno w kontekścieInt
iComplex Double
ustawieniuComplex Double
.(+) :: Num a => a -> a -> a
. Standardowe dodawanie, ogólnie rozumiane jako asocjacyjne i przemienne, tj.a + (b + c) ≡ (a + b) + c a + b ≡ b + a
(-) :: Num a => a -> a -> a
. Odejmowanie, które jest odwrotnością dodawania:(a - b) + b ≡ (a + b) - b ≡ a
(*) :: Num a => a -> a -> a
. Mnożenie, operacja asocjacyjna, która rozdziela po dodaniu:a * (b * c) ≡ (a * b) * c a * (b + c) ≡ a * b + a * c
w najczęstszych przypadkach mnożenie jest również przemienne, ale z pewnością nie jest to wymóg.
negate :: Num a => a -> a
. Pełna nazwa jednoargumentowego operatora negacji.-1
to cukier składniowy dlanegate 1
.-a ≡ negate a ≡ 0 - a
abs :: Num a => a -> a
. Funkcja wartości bezwzględnej zawsze daje wynik nieujemny o tej samej wielkościabs (-a) ≡ abs a abs (abs a) ≡ abs a
abs a ≡ 0
powinno się zdarzyć tylko wtedy,a ≡ 0
.W przypadku typów rzeczywistych jasne jest, co oznacza wartość nieujemna: zawsze masz
abs a >= 0
. Złożone typy itp. Nie mają dobrze zdefiniowanego uporządkowania, jednak wynikabs
powinien zawsze leżeć w rzeczywistym podzbiorze ‡ (tzn. Podać liczbę, która może być również zapisana jako literał liczby pojedynczej bez negacji).signum :: Num a => a -> a
. Funkcja znak, zgodnie z nazwą, daje tylko-1
lub1
, w zależności od znaku argumentu. W rzeczywistości dotyczy to tylko niezerowych liczb rzeczywistych; ogólniesignum
jest lepiej rozumiane jako funkcja normalizująca :abs (signum a) ≡ 1 -- unless a≡0 signum a * abs a ≡ a -- This is required to be true for all Num instances
Zauważ, że sekcja 6.4.4 raportu Haskell 2010 wyraźnie wymaga, aby ta ostatnia równość była ważna dla każdej ważnej instancji
Num
.
Niektóre biblioteki, zwłaszcza liniowe i hmatrixowe , mają znacznie bardziej swobodne rozumienie, do czego służy klasa Num
: traktują ją jako sposób na przeciążenie operatorów arytmetycznych . Chociaż jest to dość proste dla +
i -
, już staje się kłopotliwe z *
a bardziej z innymi metodami. Na przykład, czy *
powinno oznaczać mnożenie macierzy czy mnożenie elementów?
Prawdopodobnie złym pomysłem jest zdefiniowanie takich instancji niepoliczalnych; proszę rozważyć dedykowane klasy, takie jak VectorSpace
.
† W szczególności „negatywy” niepodpisanych typów są (-4 :: Word32) == 4294967292
dużymi dodatnimi, np. (-4 :: Word32) == 4294967292
.
‡ ta nie jest powszechnie spełniony: typy wektorowe nie mają realną podzbiór. Kontrowersyjne Num
substancje dla takich typów ogólnie definiują abs
i signum
elementarnie, co matematycznie nie ma sensu.