Haskell Language
Типы классов
Поиск…
Вступление
Typeclasses в Haskell - это средство определения поведения, связанного с типом отдельно от определения этого типа. Если, скажем, в Java, вы определяете поведение как часть определения типа - то есть в интерфейсе, абстрактном классе или конкретном классе - Haskell сохраняет эти две вещи отдельно.
В base
пакете Haskell уже определено несколько типов типов. Связь между ними проиллюстрирована в разделе «Примечания» ниже.
замечания
Следующая диаграмма, взятая из статьи Typeclassopedia, показывает взаимосвязь между различными классами в Haskell.
Возможно и класс Functor
В Haskell типы данных могут иметь аргументы, подобные функциям. Возьмите тип Maybe
например.
Maybe
, это очень полезный тип, который позволяет нам представить идею неудачи или ее возможности. Другими словами, если есть вероятность, что вычисление не удастся, мы будем использовать тип Maybe
. Maybe
похож на обертку для других типов, предоставляя им дополнительную функциональность.
Его фактическое заявление довольно просто.
Maybe a = Just a | Nothing
Это говорит о том, что « Maybe
происходит в двух формах: « Just
, который представляет успех, и « Nothing
, который представляет собой неудачу. Just
принимает один аргумент, который определяет тип Maybe
, и Nothing
принимает. Например, значение Just "foo"
будет иметь тип Maybe String
, который является строковым типом, завернутым в дополнительную функциональность Maybe
. Значение Nothing
имеет тип Maybe a
где a
может быть любым типом.
Эта идея обертывания типов, чтобы дать им дополнительную функциональность, очень полезна и применима к более чем просто Maybe
. Другие примеры включают типы Either
, IO
и типы списков, каждый из которых обеспечивает различные функциональные возможности. Однако есть некоторые действия и способности, которые являются общими для всех этих типов обертки. Наиболее заметным из них является возможность изменения инкапсулированного значения.
Общепринято думать о таких типах ящиков, которые могут иметь значения, размещенные в них. Различные блоки содержат разные значения и делают разные вещи, но ни одна из них не полезна без возможности доступа к содержимому внутри.
Чтобы инкапсулировать эту идею, Haskell поставляется со стандартным классом типа Functor
. Он определяется следующим образом.
class Functor f where
fmap :: (a -> b) -> f a -> f b
Как видно, класс имеет одну функцию fmap
из двух аргументов. Первый аргумент - это функция от одного типа, a
, от другого, b
. Второй аргумент - это функтор (тип-оболочка), содержащий значение типа a
. Он возвращает функтор (тип-обертку), содержащий значение типа b
.
Проще говоря, fmap
принимает функцию и применяется к значению внутри функтора. Это единственная функция, необходимая для того, чтобы тип был членом класса Functor
, но он чрезвычайно полезен. Функции, работающие на функторах с более конкретными приложениями, можно найти в стилях Applicative
и Monad
.
Наследование типа класса: класс типа Ord
Haskell поддерживает понятие расширения класса. Например, класс Ord
наследует все операции в Eq
, но, кроме того, имеет функцию compare
которая возвращает Ordering
между значениями. Ord
может также содержать общие операторы сравнения порядка, а также метод min
и max
метод.
Обозначение =>
имеет то же значение, что и в сигнатуре функции, и требует, чтобы тип a
реализовал Eq
, чтобы реализовать 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
Все методы следующие compare
можно извлечь из него в ряде способов:
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
Типовые классы, которые сами расширяют Ord
должны реализовать по крайней мере либо метод compare
либо метод (<=)
, который создает направленную решетку наследования.
уравнение
Все базовые типы данных (например, Int
, String
, Eq a => [a]
) из Prelude, за исключением функций и IO
имеют экземпляры Eq
. Если тип создает экземпляр Eq
это означает, что мы знаем, как сравнивать два значения для значения или структурного равенства.
> 3 == 2
False
> 3 == 3
True
Необходимые методы
-
(==) :: Eq a => a -> a -> Boolean
или(/=) :: Eq a => a -> a -> Boolean
(если только один реализован, другой по умолчанию - отрицание определенный)
Определяет
-
(==) :: Eq a => a -> a -> Boolean
-
(/=) :: Eq a => a -> a -> Boolean
Прямые суперклассы
Никто
Известные подклассы
Ord
Типичные экземпляры Ord
включают, например, Int
, String
и [a]
(для типов a
где есть экземпляр Ord a
). Если тип создает экземпляр Ord
это означает, что мы знаем «естественный» порядок значений этого типа. Обратите внимание: часто существует множество возможных вариантов «естественного» упорядочения типа, и Ord
заставляет нас это одобрить.
Ord
предоставляет стандартные (<=)
, (<)
, (>)
, (>=)
операторы, но интересно их определяет, используя собственный тип алгебраических данных
data Ordering = LT | EQ | GT
compare :: Ord a => a -> a -> Ordering
Необходимые методы
-
compare :: Ord a => a -> a -> Ordering
или(<=) :: Ord a => a -> a -> Boolean
(методcompare
по умолчанию используется(<=)
в его реализации)
Определяет
-
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
Прямые суперклассы
Monoid
Типы экземпляров Monoid
включают списки, числа и функции с возвращаемыми значениями Monoid
. Чтобы создать экземпляр Monoid
тип должен поддерживать ассоциативную двоичную операцию ( mappend
или (<>)
), которая объединяет его значения и имеет специальное «нулевое» значение ( mempty
), так что объединение значения с ним не изменяет это значение:
mempty <> x == x
x <> mempty == x
x <> (y <> z) == (x <> y) <> z
Интуитивно, типы Monoid
являются «подобными спискам», поскольку они поддерживают добавление значений вместе. В качестве альтернативы, типы Monoid
можно рассматривать как последовательности значений, для которых мы заботимся о порядке, но не о группировании. Например, двоичное дерево является Monoid
, но, используя операции Monoid
мы не можем наблюдать его ветвящуюся структуру, а только обход его значений (см. Foldable
и Traversable
).
Необходимые методы
-
mempty :: Monoid m => m
-
mappend :: Monoid m => m -> m -> m
Прямые суперклассы
Никто
Num
Самый общий класс для типов чисел, точнее для колец , то есть чисел, которые можно добавлять и вычитать и умножать в обычном смысле, но не обязательно разделять.
Этот класс содержит как интегральные типы ( Int
, Integer
, Word32
и т. Д.), Так и дробные типы ( Double
, Rational
, также комплексные числа и т. Д.). В случае конечных типов семантика обычно понимается как модульная арифметика , т. Е. С избыточным и нижним потоком .
Обратите внимание, что правила для числовых классов гораздо менее строго соблюдаются, чем законы монады или моноида, или правила сравнения равенства . В частности, числа с плавающей запятой обычно подчиняются законам только в приблизительном смысле.
Методы
fromInteger :: Num a => Integer -> a
. конвертировать целое число в общий тип номера (при необходимости обертывать диапазон). Литералы номера Haskell можно понимать как мономорфный литералInteger
с общим преобразованием вокруг него, поэтому вы можете использовать литерал5
как в контекстеInt
и в параметреComplex Double
.(+) :: Num a => a -> a -> a
. Стандартное дополнение, обычно понимаемое как ассоциативное и коммутативное, т. Е.a + (b + c) ≡ (a + b) + c a + b ≡ b + a
(-) :: Num a => a -> a -> a
. Вычитание, которое является обратным добавлению:(a - b) + b ≡ (a + b) - b ≡ a
(*) :: Num a => a -> a -> a
. Умножение, ассоциативная операция, которая является дистрибутивной над добавлением:a * (b * c) ≡ (a * b) * c a * (b + c) ≡ a * b + a * c
для наиболее распространенных экземпляров умножение также является коммутативным, но это определенно не является требованием.
negate :: Num a => a -> a
. Полное имя оператора унарного отрицания.-1
- синтаксический сахар дляnegate 1
.-a ≡ negate a ≡ 0 - a
abs :: Num a => a -> a
. Функция абсолютного значения всегда дает неотрицательный результат той же величиныabs (-a) ≡ abs a abs (abs a) ≡ abs a
abs a ≡ 0
должно произойти только, еслиa ≡ 0
.Для реальных типов ясно, какие неотрицательные средства: у вас всегда есть
abs a >= 0
. Сложные и т. Д. Типы не имеют четко определенного порядка, однако результатabs
всегда должен лежать в вещественном подмножестве ‡ (т. Е. Дать число, которое также может быть записано в виде единственного литерала без отрицания).signum :: Num a => a -> a
. Функция знака, в соответствии с именем, дает только-1
или1
, в зависимости от знака аргумента. Собственно, это верно только для ненулевых действительных чисел; в общихsignum
лучше понимать как нормализующая функцию:abs (signum a) ≡ 1 -- unless a≡0 signum a * abs a ≡ a -- This is required to be true for all Num instances
Обратите внимание, что в разделе 6.4.4 отчета Haskell 2010 явно требуется, чтобы это последнее равенство выполнялось для любого действительного экземпляра
Num
.
Некоторые библиотеки, в частности линейные и hmatrix , имеют гораздо более слабое понимание того, для чего предназначен класс Num
: они рассматривают его как способ перегрузить арифметические операторы . Хотя это довольно просто для +
и -
, это уже становится проблематичным с *
и тем более с другими методами. Например, должно *
означать матричное умножение или умножение по элементам?
Вероятно, это плохая идея для определения таких не числовых экземпляров; рассмотрите специальные классы, такие как VectorSpace
.
† В частности, «негативы» неподписанных типов обернуты вокруг большого положительного значения, например (-4 :: Word32) == 4294967292
.
‡ Это широко не выполняется: типы векторов не имеют реального подмножества. Спорные Num
состояния для таких типов обычно определяют abs
и signum
элементы, что математически не имеет смысла.