Типы и переменные типов в Haskell

Царапая на поверхности системы типа 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 и ec имеют разные типы, они оба проверяют True на == e. Я говорю "как-то" не потому, что я удивлен (я не удивлен), а потому, что я не знаю, как называется правило/механизм, который позволяет это. Это как если бы переменная типа "a" в выражении "[] == en" могла принимать значение "Num" для оценки. И, аналогично, при тестировании с "[] == ec" разрешается стать "Char".

Причина, по которой я не уверен, что моя интерпретация верна, заключается в следующем:

Prelude> (en == e) && (ec == e)
True

потому что интуитивно это подразумевает, что в одном и том же выражении e принимает оба значения Num и 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 "" - и не имеет значения, является ли он первым или вторым операндом.

Возможно, необходимо извлечь важный урок о системе типов Хаскелла. Спасибо за ваше время!

Ответ 1

Когда мы говорим, что 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 или С#, за исключением того, что вы можете объявить их позже, не обязательно вместе с типом, который их реализует.

Таким образом, подпись en :: Num a => [a] означает, что en - это список с элементами любого типа, если этот тип реализует ("имеет экземпляр") класс типов Num.

И способ вывода типов в Haskell работает так: компилятор сначала определяет наиболее конкретные типы, которые он может, а затем пытается найти реализации ("экземпляры") требуемых классов типов для этих типов.

В вашем случае компилятор видит, что en :: [a] сравнивается с ec :: [Char], и a: "О, я знаю: a должен быть Char !" Затем он находит экземпляры класса и замечает, что a должен иметь экземпляр Num, а поскольку a является Char, отсюда следует, что Char должен иметь экземпляр Num. Но это не так, и поэтому компилятор жалуется: "не могу найти (Num Char)"

Что касается "вытекающих из использования en " - хорошо, потому что en является причиной, по которой требуется экземпляр Num. en - это тот, который имеет Num в своей сигнатуре типа, так что именно его присутствие вызывает требование Num

Ответ 2

Иногда удобно рассматривать полиморфные функции как функции, принимающие аргументы явного типа. Давайте рассмотрим полиморфную функцию тождества в качестве примера.

id :: forall a . a -> a
id x = x

Мы можем думать об этой функции следующим образом:

  • первый, функция принимает в качестве входных аргументов типа имени a
  • во- вторых, функция принимает в качестве входных значений x от ранее выбранного типа a
  • наконец, функция возвращает x (типа 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 должны иметь один и тот же тип. На самом деле это не так, поскольку выводятся различные неявные аргументы типа.