Поцарапав поверхность системы типов Haskell, запустил это:
Prelude> e = []
Prelude> ec = tail "a"
Prelude> en = tail [1]
Prelude> :t e
e :: [a]
Prelude> :t ec
ec :: [Char]
Prelude> :t en
en :: Num a => [a]
Prelude> en == e
True
Prelude> ec == e
True
Каким-то образом, несмотря на то, что en и эк имеют разные типы, они оба проверяют True на == е. Я говорю «как-то» не потому, что удивлен (нет), а потому, что не знаю, как называется правило/механизм, позволяющий это делать. Это как если бы переменная типа "a" в выражении "[] == en" разрешено принимать значение "Num" для оценки. И аналогичным образом при тестировании с «[] == ec» ему разрешено стать «Char».
Причина, по которой я не уверен, что моя интерпретация верна, заключается в следующем:
Prelude> (en == e) && (ec == e)
True
, потому что интуитивно это подразумевает, что в одном и том же выражении e принимает оба значения Num и Char «одновременно» (по крайней мере, так я привык интерпретировать семантику &&). Разве что «предположение» Char действует только при оценке (ec == e), а (en == e) оценивается независимо, в отдельной... редукции? (Я предполагаю терминологию здесь).
А потом приходит это:
Prelude> en == es
<interactive>:80:1: error:
• No instance for (Num Char) arising from a use of ‘en’
• In the first argument of ‘(==)’, namely ‘en’
In the expression: en == es
In an equation for ‘it’: it = en == es
Prelude> es == en
<interactive>:81:7: error:
• No instance for (Num Char) arising from a use of ‘en’
• In the second argument of ‘(==)’, namely ‘en’
In the expression: es == en
In an equation for ‘it’: it = es == en
Не удивлен исключением, но удивлен тем, что в обоих тестах сообщение об ошибке жалуется на «использование 'en'» - и не имеет значения, первый это или второй операнд.
Возможно, необходимо извлечь важный урок о системе типов Haskell. Спасибо за уделенное время!





Когда мы говорим, что e :: [a], это означает, что e — это список элементов любого типа. Какого вида? Любой тип! Тот тип, который вам нужен в данный момент.
Если вы переходите с языка, отличного от ML, это может быть немного легче понять, сначала взглянув на функцию (а не на значение). Учти это:
f x = [x]
Тип этой функции f :: a -> [a]. Это означает, грубо говоря, что эта функция работает для любого типа a. Вы даете ему значение этого типа, и он возвращает вам список с элементами этого типа. Какого вида? Любой тип! В зависимости от того, что вам нужно.
Когда я вызываю эту функцию, я фактически выберите того типа, который мне нужен в данный момент. Если я назову это как f 'x', я выберу a = Char, а если я назову это как f True, я выберу a = Bool. Итак, важным моментом здесь является то, что тот, кто вызывает функцию, выбирает параметр типа.
Но я не обязан выбирать его только один раз и на всю вечность. Вместо этого я выбираю параметр типа каждый раз, когда я вызываю функцию. Учти это:
pair = (f 'x', f True)
Здесь я дважды вызываю f, и каждый раз выбираю разные параметры типа - первый раз выбираю a = Char, а второй раз выбираю a = Bool.
Хорошо, теперь следующий шаг: когда я выбираю параметр типа, я могу сделать это несколькими способами. В приведенном выше примере я выбираю его, передавая параметр значения нужного мне типа. Но другой способ — указать тип результата я хочу. Учти это:
g x = []
a :: [Int]
a = g 0
b :: [Char]
b = g 42
Здесь функция g игнорирует свой параметр, поэтому нет никакой связи между ее типом и результатом g. Но я все еще могу выбрать тип этого результата, ограничивая его окружающим контекстом.
А теперь умственный скачок: функция без каких-либо параметров (также известная как «значение») ничем не отличается от функции с параметрами. У него просто нулевые параметры, вот и все.
Если у значения есть параметры типа (например, ваше значение e), я могу выбирать этот параметр типа каждый раз, когда я «вызываю» это значение так же легко, как если бы это была функция. Таким образом, в выражении e == ec && e == en вы просто «вызываете» значение e дважды, выбирая разные параметры типа при каждом вызове — так же, как я сделал в примере pair выше.
Путаница с Num — это совсем другое дело.
Видите ли, Num — это не тип. Это тип класса. Классы типов похожи на интерфейсы в Java или C#, за исключением того, что вы можете объявить их позже, не обязательно вместе с типом, который их реализует.
Таким образом, подпись en :: Num a => [a] означает, что en представляет собой список с элементами типа любой, если этот тип реализует («имеет экземпляр») класс типов Num.
Вывод типов в Haskell работает следующим образом: компилятор сначала определяет наиболее конкретные типы, которые он может, а затем пытается найти реализации («экземпляры») требуемых классов типов для этих типов.
В вашем случае компилятор видит, что en :: [a] сравнивается с ec :: [Char], и вычисляет: "о, я знаю: a должно быть Char!" Затем он ищет экземпляры класса и замечает, что a должен иметь экземпляр Num, а поскольку a — это Char, отсюда следует, что Char должен иметь экземпляр Num. Но это не так, поэтому компилятор жалуется: "не могу найти (Num Char)"
Что касается «возникающего из-за использования en» — ну, это потому, что en является причиной того, что экземпляр Num требуется. en - это тот, у которого есть Num в подписи типа, поэтому его присутствие вызывает требование Num
Иногда удобно думать о полиморфных функциях как о функциях, принимающих явный аргументы типа. Рассмотрим в качестве примера функцию полиморфной идентичности.
id :: forall a . a -> a
id x = x
Мы можем думать об этой функции следующим образом:
ax ранее выбранного типа ax (типа a)Вот возможный звонок:
id @Bool True
Выше синтаксис @Bool передает Bool в качестве первого аргумента (аргумент типа a), а True передается в качестве второго аргумента (x типа a = Bool).
Несколько других:
id @Int 42
id @String "hello"
id @(Int, Bool) (3, True)
Мы можем даже частично применить id, передав только аргумент типа:
id @Int :: Int -> Int
id @String :: String -> String
...
Теперь обратите внимание, что в большинстве случаев Haskell позволяет нам опустить аргумент типа. т.е. мы можем написать id "hello", и GHC попытается определить отсутствующий аргумент типа. Грубо это работает следующим образом: id "hello" преобразуется в id @t "hello" для некоторого неизвестного типа t, тогда в соответствии с типом id этот вызов может только проверить тип, если "hello" :: t, а так как "hello" :: String, мы можем вывести t = String.
Вывод типов чрезвычайно распространен в Haskell. Программисты редко указывают свои аргументы типа и позволяют GHC делать свою работу.
В твоем случае:
e :: forall a . [a]
e = []
ec :: [Char]
ec = tail "1"
en :: [Int]
en = tail [1]
Переменная e привязана к полиморфному значению. То есть на самом деле это функция сортировки, которая принимает аргумент типа a (который также можно опустить) и возвращает список типа [a].
Вместо этого ec не принимает никаких аргументов типа. Это обычный список типа [Char]. Аналогично для en.
Затем мы можем использовать
ec == (e @Char) -- both of type [Char]
en == (e @Int) -- both of type [Int]
Или мы можем позволить механизму вывода типов определить аргументы неявного типа.
ec == e -- @Char inferred
en == e -- @Int inferred
Последнее может ввести в заблуждение, поскольку кажется, что ec,e,en должен иметь один и тот же тип. На самом деле это не так, поскольку выводятся разные аргументы неявного типа.
Отличное доступное объяснение параметрического полиморфизма!