it-swarm.com.ru

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

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

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

Что с этим? Почему у меня не может быть двух функций с одинаковым именем, но работающих на разных типах ... Исходя из C++, я нахожу это очень странным. Но у меня, вероятно, неправильные представления о том, что на самом деле представляют собой эти вещи. но как только я заверну их в эти экземпляры класса типов, я смогу.

Не стесняйтесь подбрасывать мне категории или вводить теоретические слова, так как я изучаю эти предметы параллельно с изучением Haskell, и я подозреваю, что в них есть теоретическая основа для того, как Haskell делает вещи здесь.

16
lo tolmencre

Я согласен с большинством из ответа Виллема Ван Онсема , но я думаю, что он упускает одно из главных преимуществ классов типов по сравнению с действительно специальной перегрузкой: abstraction. Представьте, что мы использовали специальную перегрузку вместо классов типов для определения операций 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, которая работает с монадой any:

join x = x >>= id

Однако сейчас у нас проблема. Какого типа join?

Ясно, что join должен быть полиморфным, так как он работает с любой монадой по дизайну. Но давать ему сигнатуру типа forall m a. m (m a) -> m a, очевидно, было бы неправильно, поскольку она не работает работает для всех типов, только для монадических. Следовательно, нам нужно что-то в нашем типе, которое выражает необходимость существования некоторой операции (>>=) :: m a -> (a -> m b) -> m b, что именно то, что обеспечивает ограничение класса типов.

Учитывая это, становится ясно, что специальная перегрузка позволяет перегрузить имена, но невозможно абстрагироваться от этих перегруженных имен, потому что нет никакой гарантии, что различные реализации связаны каким-либо образом. Вы могли определять монады без классов типов, но тогда вы не могли определить join, when, unless, mapM, sequence и все другие полезные вещи, которые вы получаете бесплатно, когда вы определяете только две операции.

Следовательно, классы типов необходимы в Haskell, чтобы обеспечить возможность повторного использования кода и избежать колоссального дублирования. Но могли бы вы иметь {оба перегрузку в стиле классов и специальную переадресацию имен? Да, и фактически, Идрис делает. Но типичный вывод Идриса очень отличается от типичного в Хаскеле, поэтому его более целесообразно поддерживать, чем в Хаскелле, по многим причинам в ответе Виллема.

36
Alexis King

Короче говоря: потому что именно так был разработан Haskell.

Зачем ставить (Eq a) в подписи. Если == не определен для a, то почему бы просто не выдать ошибку при обнаружении a == b?

Почему мы помещаем типы в сигнатуру программы на C++ (а не просто как утверждение в теле)? Потому что так устроен C++. Как правило, концепция того, на чем строятся языки программирования: «сделать явным то, что должно быть явным».

Не сказано, что модуль 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. В результате мы получили бы много ошибок, которые обнаруживаются только во время компиляции (или, если бы Haskell был динамическим языком во время выполнения). Идея поместить Eq a в сигнатуру типа состоит в том, что мы можем заранее просмотреть сигнатуру foo и таким образом убедиться, что типы являются экземпляром класса типов.

Обратите внимание, что вам не нужно писать сигнатуры типов самостоятельно: обычно Haskell может получить сигнатуру функции, но сигнатура должна включать всю необходимую информацию для эффективного вызова и использования функции. Добавляя ограничения типов, мы ускоряем разработку.

Что с этим? Почему у меня не может быть двух функций с одинаковым именем, но работающих на разных типах.

Опять же: так устроен Haskell. Функции в функциональных языках программирования являются "гражданами первого класса". Это означает, что у них обычно есть имя, и мы хотим как можно больше избегать конфликтов имен. Так же, как классы в C++ обычно имеют уникальное имя (за исключением пространств имен).

Скажем, вы бы определили две разные функции:

incr :: Int -> Int
incr = (+1)

incr :: Bool -> Bool
incr _ = True

bar = incr

Тогда какой incr нужно выбрать bar? Конечно, мы можем сделать типы явными (то есть incr :: Bool -> Bool), но обычно мы хотим избежать этой работы, так как она создает много шума.

Еще одна веская причина, по которой мы этого не делаем, заключается в том, что обычно класс типов - это не просто набор функций: он добавляет контракты к этим функциям. Например, класс типов Monad должен удовлетворять определенным отношениям между функциями. Например, (>>= return) должен быть эквивалентен id. Другими словами, класс типов:

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a

Не описывает две функции independent(>>=) и return: это набор функций. У вас есть оба (обычно с некоторыми договорами между определенными >>= и return), или ни одного из них вообще.

16
Willem Van Onsem

Это только отвечает на вопрос 1 (по крайней мере, напрямую).

Подпись типа f :: a -> a -> Bool является сокращением для f :: forall a. a -> a -> Bool. f не будет действительно работать для всех типов a, если он работает только для as, для которых определен (==). Это ограничение для типов, имеющих (==), выражается с помощью ограничения (Eq a) в f :: forall a. (Eq a) => a -> a -> Bool.

«Для всех»/универсальная квантификация лежит в основе (параметрического) полиморфизма Хаскелла и, среди прочего, обеспечивает мощные и важные свойства параметричности .

5
David

Хаскель придерживается двух аксиом (среди прочих):

  1. Каждая переменная может использоваться как выражение самостоятельно;
  2. Каждое выражение имеет тип, который точно определяет, что вам разрешено делать с ним.

Если у тебя есть

f :: A -> A

а также

f :: B -> B

тогда, в соответствии с принципами, принятыми в Haskell, f по-прежнему будет допустимым выражением, которое само по себе должно иметь тип single . Хотя это можно сделать с использованием подтипов, это было гораздо сложнее, чем решение класса типов.

Точно так же потребность в Eq a в

(==) :: Eq a => a -> a -> Bool

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

1
Jonathan Cast