Типы и тип переменной в 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 и эк имеют разные типы, они оба проверяют 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. Спасибо за уделенное время!

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
0
2 195
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Когда мы говорим, что 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

Отличное доступное объяснение параметрического полиморфизма!

luqui 27.02.2019 08:45

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

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

Другие вопросы по теме