Типоразмеры и перегрузка, какая связь?
В настоящее время я пытаюсь обернуть голову стилями и экземплярами, и я пока не совсем понимаю их. До сих пор у меня есть два вопроса:
1) Почему необходимо иметь класс типа в сигнатуре функции, когда функция использует некоторую функцию из этого типа. Пример:
f :: (Eq a) => a -> a -> Bool
f a b = a == b
Зачем ставить (Eq a)
в подпись. Если ==
не определено для a
, то почему бы просто не выбросить ошибку при встрече a == b
? В чем смысл объявлять класс типа впереди?
2) Как относятся классы типов и функции перегрузки?
Это невозможно сделать:
data A = A
data B = B
f :: A -> A
f a = a
f :: B -> B
f b = b
Но это можно сделать:
data A = A
data B = B
class F a where
f :: a -> a
instance F A where
f a = a
instance F B where
f b = b
Что с этим связано? Почему у меня нет двух функций с тем же именем, но они работают на разных типах... Исходя из С++, я нахожу это очень странным. Но у меня, вероятно, есть неправильные представления о том, что это на самом деле. но как только я завершу их в эти типы экземпляров класса типа, я могу.
Не стесняйтесь бросать категорию или набирать теоретические слова у меня, так как я изучаю эти предметы параллельно с обучением Haskell, и я подозреваю, что в них есть теоретическая основа для того, как Haskell здесь делает что-то.
Ответы
Ответ 1
Я согласен с большей частью ответа Виллема Ван Насема, но я думаю, что он упускает из виду одно из главных преимуществ классных списков по сравнению с действительно ad-hoc перегрузкой: абстракция. Представьте, что мы использовали ad-hoc перегрузку вместо typeclasses для определения операций Monad
:
-- Maybe
pure :: a -> Maybe a
pure = Just
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Just x >>= f = f x
Nothing >>= _ = Nothing
-- Either
pure :: a -> Either e a
pure = Right
(>>=) :: Either e a -> (a -> Either e b) -> Either e b
Right x >>= f = f x
Left err >>= _ = Left err
Теперь мы знаем, что каждая монада может быть выражена через pure
и >>=
, как указано выше, но мы также знаем, что их можно выразить эквивалентно с помощью fmap
, pure
и join
, Поэтому мы должны иметь возможность реализовать функцию join
, которая работает на любой монаде:
join x = x >>= id
Однако теперь у нас есть проблема. Что такое join
s типа?
Ясно, что join
должен быть полиморфным, поскольку он работает на любой монаде по дизайну. Но присвоение ему сигнатуры типа forall m a. m (m a) -> m a
, очевидно, было бы неправильным, так как оно не работает для всех типов, только монадических. Поэтому нам нужно что-то в нашем типе, которое выражает необходимость существования какой-либо операции (>>=) :: m a -> (a -> m b) -> m b
, что и обеспечивает ограничение типа typeclass.
Учитывая это, становится ясно, что перегрузка ad-hoc позволяет перегружать имена, но невозможно абстрагироваться от перегруженных имен, потому что нет никакой гарантии, что разные реализации связаны каким-либо образом. Вы можете определить monads без typeclasses, но тогда вы не могли бы определить join
, when
, unless
, mapM
, sequence
и все другие приятные вещи, которые вы получаете бесплатно, когда вы определяете только две операции.
Следовательно, в Haskell необходимы классные классы для повторного использования кода и предотвращения огромного количества дублирования. Но могли ли вы иметь перегрузку типа "тип-стиль" и перегрузку имен на основе типа, ad-hoc? Да, и на самом деле, Идрис. Но вывод типа Idriss сильно отличается от Haskells, поэтому его более целесообразно поддерживать, чем в Haskell по многим причинам в ответе Виллема.
Ответ 2
Короче: , потому что именно так Haskell был разработан.
Зачем ставить (Eq a)
в подпись. Если ==
не определено для a, то почему бы просто не выбросить ошибку при встрече a == b
?
Почему мы помещаем типы в подпись программы на С++ (а не только где-то как утверждение в теле)? Потому что именно так разработан С++. Как правило, понятие о том, какие языки программирования построены, - "четко указать, что должно быть явным".
Не сказано, что модуль Haskell является открытым исходным кодом. Значит, мы имеем только доступную подпись. Таким образом, это означает, что, когда мы, например, пишем:
Prelude> foo A A
<interactive>:4:1: error:
• No instance for (Eq A) arising from a use of ‘foo’
• In the expression: foo A A
In an equation for ‘it’: it = foo A A
Мы часто пишем foo
здесь с типами, которые не имеют Eq
typeclass. В результате мы получили бы много ошибок, которые были обнаружены только во время компиляции (или если Haskell был динамическим языком во время выполнения). Идея размещения Eq a
в сигнатуре типа заключается в том, что мы можем заранее найти подпись foo
и, таким образом, убедиться, что типы являются экземплярами класса типов.
Обратите внимание, что вам не нужно писать сигнатуры типов самостоятельно: Haskell обычно может выводить подпись функции, но подпись должна включать всю необходимую информацию для вызова и эффективного использования функции. Добавляя ограничения типа, мы ускоряем разработку.
Что с этим связано? Почему у меня нет двух функций с тем же именем, но они работают на разных типах.
Опять же: так спроектирован Haskell. Функции на языках функционального программирования - "граждане первого класса". Это означает, что у них обычно есть имя, и мы хотим как можно больше избегать конфликтов имен. Подобно тому, как классы на С++ обычно имеют уникальное имя (кроме пространств имен).
Скажем, вы бы определили две разные функции:
incr :: Int -> Int
incr = (+1)
incr :: Bool -> Bool
incr _ = True
bar = incr
Тогда какой incr
должен был бы bar
выбрать? Конечно, мы можем сделать типы явными (т.е. incr :: Bool -> Bool
), но обычно мы хотим избежать этой работы, поскольку она вводит много шума.
Еще одна веская причина, по которой мы этого не делаем, заключается в том, что обычно класс классов - это не просто набор функций: он добавляет контракты к этим функциям. Например, класс Monad
должен удовлетворять определенным отношениям между функциями. Например, (>>= return)
должен быть эквивалентен id
. Другими словами, typeclass:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
Не описывает две независимые функции (>>=)
и return
: это набор функций. У вас есть они оба (обычно с некоторыми контрактами между конкретными >>=
и return
), или ни один из них вообще.
Ответ 3
Это отвечает только на вопрос 1 (по крайней мере, прямо).
Типовая подпись f :: a -> a -> Bool
является сокращением для f :: forall a. a -> a -> Bool
. f
не будет действительно работать для всех типов a
, если он работает только для a
, которые имеют (==)
. Это ограничение для типов, имеющих (==)
, выражается с помощью ограничения (Eq a)
в f :: forall a. (Eq a) => a -> a -> Bool
.
"Для всех" /универсальное квантификация лежит в основе Haskell (параметрического) полиморфизма и, среди прочего, обеспечивает мощные и важные свойства parametricity.
Ответ 4
Haskell придерживается двух аксиом (среди прочих):
- Каждая переменная может использоваться как самостоятельное выражение;
- Каждое выражение имеет тип, который точно определяет, что вам разрешено делать с ним.
Если у вас
f :: A -> A
и
f :: B -> B
то, согласно принципам, принятым в Haskell, f
все равно будет правильным выражением, которое по-прежнему должно иметь один тип. Хотя это возможно сделать с использованием подтипирования, это было сочтено гораздо более сложным, чем решение типа класса.
Аналогично, необходимость Eq a
в
(==) :: Eq a => a -> a -> Bool
происходит от того, что тип ==
должен полностью описать, что вы можете с ним сделать. Если вы можете только вызывать его в определенных типах, подпись типа должна отражать это.