Типоразмеры и перегрузка, какая связь?

В настоящее время я пытаюсь обернуть голову стилями и экземплярами, и я пока не совсем понимаю их. До сих пор у меня есть два вопроса:

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

происходит от того, что тип == должен полностью описать, что вы можете с ним сделать. Если вы можете только вызывать его в определенных типах, подпись типа должна отражать это.