Что означает, что семантика (Haskell) зависит от выведенных типов (полиморфизма возвращаемого типа)?

Здесь комментатор пишет:

Наконец, если у вас достаточно макромагии, это может быть сделано... но, вероятно, теперь меньше усилий, чем реализация типа типа типа Haskell на вершине Clojure. Типизированный Clojure может быть отличной моделью для этого, за исключением того, что он явно разработан так, что семантика Clojure не может быть затронута предполагаемыми типами. Это именно то, что происходит в полиморфизме возвращаемого типа, и поэтому это явно невозможно в Typed Clojure.

Мой вопрос - Что означает, что семантика (Haskell) подвержена влиянию выведенных типов (полиморфизма возвращаемого типа)?

Ответ 1

Рассмотрим функцию read, которая имеет (ad-hoc) полиморфное возвращаемое значение:

read :: (Read a) => String -> a

Реализация не так важна. Единственная важная часть заключается в том, что реализация зависит от экземпляра read, выбранного во время компиляции, и что вывод может привести к тому, что разные типы будут выбраны для одного и того же вызова read.

addFive :: Int -> Int
addFive x = x + 5

main :: IO ()
main = do
    print (addFive (read "11"))
    putStrLn (read "11")

Там вызов read с тем же аргументом дважды. Haskell требует ссылочной прозрачности, поэтому он должен приводить к тому же самому и в оба раза, правильно? Ну, не совсем. Требуется тип предполагаемого возврата. В строке print выводимый тип возврата Int. В строке putStrLn выводимый тип возврата String. И поскольку это ad-hoc полиморфно, семантика изменяется вместе с переменной типа.

Откроется строка print 16. Линия putStrLn выйдет из строя, потому что "11" не является входом, который read успешно декодирует в String.

И поскольку переменная type появляется только в возвращаемом типе, в момент вызова функции нет значения этого типа. Нет способа отправить значение типа во время выполнения, чтобы выяснить, какой экземпляр read использовать. Единственный способ понять это - узнать тип во время компиляции. Так что Typed Clojure не может этого сделать - это означает, что семантика зависит от типов времени компиляции.

Редактирует адрес комментария

Я понятия не имею, если он должен произвести на вас впечатление. Но так как ваше утверждение (2) ошибочно во всех отношениях, это указывает на явное отсутствие основ для понимания этого примера. Я думаю, мне нужно вернуться к тому, что означает переменная типа в Haskell, чтобы объяснить это.

Переменная типа в Haskell представляет неизвестный, но конкретный тип, выбранный вызывающим. Тип Read a => String -> a не означает, что функция выбирает тип для возвращаемого значения на основе его ввода. Это означает, что функция выбирает, как она будет работать в зависимости от типа, который он выдает.

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

Ваше утверждение (2) совершенно неверно. Программа не сбой, потому что read возвращает Int, где ожидаемый код a String, и происходит что-то вроде ClassCastException. Программа вылетает из-за того, что read выбрал синтаксический анализатор для разбора String литералов на основе его возвращаемого типа во время компиляции, но введенный им вход был недействительным литералом String. (Напротив, "\"11\"" является допустимым литералом String, потому что он цитируется.)

Полужирная часть - важная часть. Функция read выбирает, какой парсер он будет использовать во время компиляции, исходя из типа возврата. Это и очень мощный метод, и то, что вы не можете сделать с Typed Clojure.

Ответ 2

Один из способов увидеть это различие - изучить систему F. Это довольно похоже на Haskell, за исключением того, что весь полиморфизм явно введен с использованием "type lambdas". Типичная нотация заключается в том, что тип lambdas появляется в объявлениях типа как "forall" квантификация (я напишу \/) и в значениях как "большие lambdas" (напишу /\).

Итак, например, id становится

id :: \/ a . a -> a
id = /\ type -> \x -> x

Итак, мы должны явно передать тип, создающий экземпляр переменной a, чтобы использовать id. Вы можете увидеть, что это используется как

> id Int 3
3 :: Int

Итак, что это связано с полиморфизмом возвращаемого типа? Ну, система Haskell типа inferencer (система Hindley-Milner) может рассматриваться как по существу, живущая на System F, автоматически обтекающая типы. Для этого он ограничивает большую гибкость системы F, но пока мы будем игнорировать это.

Все, что вам нужно иметь в виду, это то, что тип inferencer определяет, какие переменные типа должны быть созданы как все до выполнения. Или, что более ясно, во время выполнения все типы лямбда исключены. Это то, что позволяет разделить фазу времени компиляции/времени выполнения, что позволяет стирать стили.


Хаскелл расширяет Хиндли-Милнера, чтобы позволить своего рода ограниченный полиморфизм. Тип

\/ a . C a => a

говорит, что тип лямбда может выполняться только типами, которые ограничены C. Затем Haskell решает уравнения об этих границах, чтобы определить правильные типы для вставки где угодно.

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

f :: a -> b
e :: a

f e :: b

если возвращаемый тип функции может ограничить переменные типа, то он будет. Это позволит оппоненту выбрать правильный вариант System F. Затем во время выполнения все типы lambdas исчезают, и остается только точный, без кода, код, который соответствует требуемому типу возвращаемого значения.