Я не очень разбираюсь в Haskell, поэтому это может быть очень простой вопрос.
Какие языковые ограничения снимает Rank2Types ? Разве функции в Haskell не поддерживают полиморфные аргументы?
haskell
types
polymorphism
higher-rank-types
Андрей Щекин
источник
источник
Ответы:
Они это делают, но только с рангом 1. Это означает, что, хотя вы можете написать функцию, которая принимает разные типы аргументов без этого расширения, вы не можете написать функцию, которая использует свой аргумент как разные типы в одном и том же вызове.
Например, следующую функцию нельзя ввести без этого расширения, потому что
g
она используется с разными типами аргументов в определенииf
:Обратите внимание, что вполне возможно передать полиморфную функцию в качестве аргумента другой функции. Так что что-то вроде
map id ["a","b","c"]
совершенно законно. Но функция может использовать его только как мономорфный. В примереmap
используется,id
как если бы он имел типString -> String
. И, конечно же, вы также можете передать простую мономорфную функцию данного типа вместоid
. Без rank2types функция не может требовать, чтобы ее аргумент был полиморфной функцией, и, следовательно, не может использовать его как полиморфную функцию.источник
f' g x y = g x + g y
. Его предполагаемый тип ранга 1 равенforall a r. Num r => (a -> r) -> a -> a -> r
. Поскольку онforall a
находится за пределами функциональных стрелок, вызывающий должен сначала выбрать тип дляa
; если они выберутInt
, мы получимf' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
, и теперь мы исправилиg
аргумент, так что он может принимать,Int
но не можетString
. Если мы включим,RankNTypes
мы сможем аннотироватьf'
типомforall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. Но не могу его использовать - что быg
?Трудно понять полиморфизм более высокого ранга, если вы не изучаете Систему F напрямую, потому что Haskell спроектирован так, чтобы скрыть детали этого от вас в интересах простоты.
Но, по сути, грубая идея состоит в том, что полиморфные типы на самом деле не имеют той
a -> b
формы, которую они имеют в Haskell; на самом деле они выглядят так, всегда с явными квантификаторами:Если вы не знаете символа «∀», он читается как «для всех»;
∀x.dog(x)
означает «для всех x, x - собака». «Λ» - заглавная лямбда, используемая для абстрагирования по параметрам типа; во второй строке говорится, что id - это функция, которая принимает типt
, а затем возвращает функцию, параметризованную этим типом.Видите ли, в системе F вы не можете сразу применить такую функцию
id
к значению; сначала вам нужно применить Λ-функцию к типу, чтобы получить λ-функцию, которую вы применяете к значению. Так например:Стандартный Haskell (например, Haskell 98 и 2010) упрощает это для вас, не имея каких-либо из этих квантификаторов типов, заглавных лямбда-выражений и приложений типов, но за кулисами GHC вставляет их, когда анализирует программу для компиляции. (Полагаю, это все время компиляции, без дополнительных затрат времени выполнения.)
Но автоматическая обработка этого в Haskell означает, что он предполагает, что «∀» никогда не появляется в левой ветви типа функции («→»).
Rank2Types
иRankNTypes
отключите эти ограничения и позвольте вам переопределить правила Haskell по умолчанию для того, где вставитьforall
.Зачем вам это нужно? Потому что полная, неограниченная Система F чертовски мощна и может делать много крутых вещей. Например, скрытие типов и модульность могут быть реализованы с использованием типов более высокого ранга. Возьмем, к примеру, простую старую функцию следующего типа rank-1 (для установки сцены):
Для использования
f
вызывающий сначала должен выбрать, какие типы использовать,r
аa
затем предоставить аргумент результирующего типа. Итак, вы можете выбратьr = Int
иa = String
:Но теперь сравните это со следующим типом более высокого ранга:
Как работает функция этого типа? Что ж, чтобы использовать его, сначала вы указываете, для какого типа использовать
r
. Скажем, мы выбираемInt
:Но теперь он
∀a
находится внутри стрелки функции, поэтому вы не можете выбрать, для какого типа использоватьa
; необходимо обращатьсяf' Int
к Λ-функции соответствующего типа. Это означает, что реализацияf'
должна выбирать, какой тип использоватьa
, а не вызывающий объектf'
. Напротив, без типов более высокого ранга вызывающий всегда выбирает типы.Для чего это полезно? На самом деле, для многих вещей, но одна идея состоит в том, что вы можете использовать это для моделирования таких вещей, как объектно-ориентированное программирование, где «объекты» объединяют некоторые скрытые данные вместе с некоторыми методами, которые работают со скрытыми данными. Так, например, объект с двумя методами - один, который возвращает,
Int
а другой, -String
может быть реализован с этим типом:Как это работает? Объект реализован как функция, которая имеет некоторые внутренние данные скрытого типа
a
. Чтобы фактически использовать объект, его клиенты передают функцию обратного вызова, которую объект будет вызывать двумя методами. Например:Здесь мы, по сути, вызываем второй метод объекта, тип которого
a → String
неизвестенa
. Что ж, неизвестноmyObject
клиентам России; но эти клиенты знают из подписи, что они смогут применить к нему любую из двух функций и получить либо a,Int
либоString
.Для реального примера Haskell ниже приведен код, который я написал, когда учился
RankNTypes
. Это реализует тип, называемый,ShowBox
который связывает вместе значение некоторого скрытого типа вместе с егоShow
экземпляром класса. Обратите внимание, что в примере внизу я составляю списокShowBox
, первый элемент которого состоит из числа, а второй - из строки. Поскольку типы скрыты с использованием типов более высокого ранга, это не нарушает проверку типов.PS: для всех, кто читает это, кто задается вопросом, почему
ExistentialTypes
GHC используетforall
, я считаю, что причина в том, что он использует такого рода технику за кулисами.источник
exists
ключевое слово, вы могли бы определить экзистенциальный тип как (например)data Any = Any (exists a. a)
, гдеAny :: (exists a. a) -> Any
. Используя ∀xP (x) → Q ≡ (∃xP (x)) → Q, мы можем сделать вывод, чтоAny
он также может иметь тип,forall a. a -> Any
и вот откуда взялосьforall
ключевое слово. Я считаю, что экзистенциальные типы, реализованные в GHC, являются просто обычными типами данных, которые также содержат все необходимые словари классов типов (извините, я не смог найти ссылку, подтверждающую это).data ApplyBox r = forall a. ApplyBox (a -> r) a
; когда вы выполняете сопоставление с шаблономApplyBox f x
, вы получаетеf :: h -> r
иx :: h
для «скрытого» ограниченного типаh
. Если я правильно понимаю, регистр словаря класса типов переводится примерно так:data ShowBox = forall a. Show a => ShowBox a
переводится примерно такdata ShowBox' = forall a. ShowBox' (ShowDict' a) a
;instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
;show' :: ShowDict a -> a -> String
.Ответ Луиса Касильяса дает много полезной информации о том, что означают типы ранга 2, но я просто остановлюсь на одном моменте, который он не затронул. Требование, чтобы аргумент был полиморфным, не только позволяет использовать его с несколькими типами; он также ограничивает то, что эта функция может делать со своим аргументом (ами) и как она может производить свой результат. То есть это дает вызывающему абоненту меньшую гибкость. Почему вы хотите это сделать? Начну с простого примера:
Допустим, у нас есть тип данных
и мы хотим написать функцию
который принимает функцию, которая должна выбрать один из элементов из данного списка и вернуть
IO
действие, запускающее ракеты по этой цели. Мы могли бы датьf
простой тип:Проблема в том, что мы могли случайно запустить
и тогда у нас будут большие проблемы! Придание
f
полиморфному типу ранга 1совершенно не помогает, потому что мы выбираем тип
a
при вызовеf
, а просто специализируемся на немCountry
и\_ -> BestAlly
снова используем наш злонамеренный . Решение - использовать тип ранга 2:Теперь функция, которую мы передаем, должна быть полиморфной, поэтому
\_ -> BestAlly
проверка типа не требуется ! Фактически, ни одна функция, возвращающая элемент, не входящий в данный список, не будет проверять тип (хотя некоторые функции, которые входят в бесконечные циклы или вызывают ошибки и поэтому никогда не возвращаются, будут делать это).Вышеизложенное, конечно, надумано, но изменение этой техники является ключом к обеспечению
ST
безопасности монады.источник
Типы более высокого ранга не так уж экзотичны, как предполагают другие ответы. Вы не поверите, но многие объектно-ориентированные языки (включая Java и C #!) Имеют их. (Конечно, никто в этих сообществах не знает их под пугающим названием «высокопоставленные типы».)
Например , я собираюсь дать это реализация учебника шаблона Visitor, который я использую все время в моей повседневной работе. Этот ответ не предназначен для ознакомления с шаблоном посетителя; эти знания легко доступны в другом месте .
В этом дурацком воображаемом HR-приложении мы хотим работать с сотрудниками, которые могут быть постоянными сотрудниками на полную ставку или временными подрядчиками. Мой предпочтительный вариант шаблона посетителя (и действительно тот, который имеет отношение к нему
RankNTypes
) параметризует тип возвращаемого значения посетителя.Дело в том, что несколько посетителей с разными типами возврата могут работать с одними и теми же данными. Это средство не
IEmployee
должно выражать мнения о том, чтоT
должно быть.Хочу обратить ваше внимание на типы. Обратите внимание, что он
IEmployeeVisitor
универсально количественно определяет свой возвращаемый тип, тогда какIEmployee
количественно оценивает его внутри своегоAccept
метода, то есть на более высоком уровне. Неуклюжий перевод с C # на Haskell:Вот и все. Типы более высокого ранга появляются в C #, когда вы пишете типы, содержащие универсальные методы.
источник
Слайды из курса Брайана О'Салливана по Haskell в Стэнфорде помогли мне понять
Rank2Types
.источник
Для тех, кто знаком с объектно-ориентированными языками, функция более высокого ранга - это просто универсальная функция, которая ожидает в качестве аргумента другую универсальную функцию.
Например, в TypeScript вы можете написать:
Видите, как универсальный тип функции
Identify
требует универсальной функции этого типаIdentifier
? Это делаетIdentify
функцию более высокого ранга.источник
Accept
имеет полиморфный тип ранга 1, но это методIEmployee
, который сам является рангом 2. Если кто-то даст мнеIEmployee
, я могу открыть его и использовать егоAccept
метод в любом типе.Visitee
классу, который вы представляете. Поf :: Visitee e => T e
сути, функция (после обессахаривания материала класса)f :: (forall r. e -> Visitor e r -> r) -> T e
. Haskell 2010 позволяет вам обойтись ограниченным полиморфизмом второго ранга, используя подобные классы.forall
моем примере вы не можете выпустить . У меня нет справочника, но вы вполне можете найти что-нибудь в "Избавьтесь от классов вашего типа" . Полиморфизм более высокого ранга действительно может вызвать проблемы с проверкой типов, но ограниченная сортировка, неявная в системе классов, - это нормально.