Типы и переменные типов в 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
должны иметь один и тот же тип. На самом деле это не так, поскольку выводятся различные неявные аргументы типа.