Я не очень разбираюсь в Haskell, поэтому это может быть очень простой вопрос.
Какие языковые ограничения снимает Rank2Types ? Разве функции в Haskell не поддерживают полиморфные аргументы?
Я не очень разбираюсь в Haskell, поэтому это может быть очень простой вопрос.
Какие языковые ограничения снимает Rank2Types ? Разве функции в Haskell не поддерживают полиморфные аргументы?
Ответы:
Разве функции в Haskell не поддерживают полиморфные аргументы?
Они это делают, но только с рангом 1. Это означает, что, хотя вы можете написать функцию, которая принимает разные типы аргументов без этого расширения, вы не можете написать функцию, которая использует свой аргумент как разные типы в одном и том же вызове.
Например, следующую функцию нельзя ввести без этого расширения, потому что g
она используется с разными типами аргументов в определении f
:
f g = g 1 + g "lala"
Обратите внимание, что вполне возможно передать полиморфную функцию в качестве аргумента другой функции. Так что что-то вроде 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; на самом деле они выглядят так, всегда с явными квантификаторами:
id :: ∀a.a → a
id = Λt.λx:t.x
Если вы не знаете символа «∀», он читается как «для всех»; ∀x.dog(x)
означает «для всех x, x - собака». «Λ» - заглавная лямбда, используемая для абстрагирования по параметрам типа; во второй строке говорится, что id - это функция, которая принимает тип t
, а затем возвращает функцию, параметризованную этим типом.
Видите ли, в системе F вы не можете сразу применить такую функцию id
к значению; сначала вам нужно применить Λ-функцию к типу, чтобы получить λ-функцию, которую вы применяете к значению. Так например:
(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
= 5
Стандартный Haskell (например, Haskell 98 и 2010) упрощает это для вас, не имея каких-либо из этих квантификаторов типов, заглавных лямбда-выражений и приложений типов, но за кулисами GHC вставляет их, когда анализирует программу для компиляции. (Полагаю, это все время компиляции, без дополнительных затрат времени выполнения.)
Но автоматическая обработка этого в Haskell означает, что он предполагает, что «∀» никогда не появляется в левой ветви типа функции («→»). Rank2Types
и RankNTypes
отключите эти ограничения и позвольте вам переопределить правила Haskell по умолчанию для того, где вставить forall
.
Зачем вам это нужно? Потому что полная, неограниченная Система F чертовски мощна и может делать много крутых вещей. Например, скрытие типов и модульность могут быть реализованы с использованием типов более высокого ранга. Возьмем, к примеру, простую старую функцию следующего типа rank-1 (для установки сцены):
f :: ∀r.∀a.((a → r) → a → r) → r
Для использования f
вызывающий сначала должен выбрать, какие типы использовать, r
а a
затем предоставить аргумент результирующего типа. Итак, вы можете выбрать r = Int
и a = String
:
f Int String :: ((String → Int) → String → Int) → Int
Но теперь сравните это со следующим типом более высокого ранга:
f' :: ∀r.(∀a.(a → r) → a → r) → r
Как работает функция этого типа? Что ж, чтобы использовать его, сначала вы указываете, для какого типа использовать r
. Скажем, мы выбираем Int
:
f' Int :: (∀a.(a → Int) → a → Int) → Int
Но теперь он ∀a
находится внутри стрелки функции, поэтому вы не можете выбрать, для какого типа использовать a
; необходимо обращаться f' Int
к Λ-функции соответствующего типа. Это означает, что реализация f'
должна выбирать, какой тип использовать a
, а не вызывающий объектf'
. Напротив, без типов более высокого ранга вызывающий всегда выбирает типы.
Для чего это полезно? На самом деле, для многих вещей, но одна идея состоит в том, что вы можете использовать это для моделирования таких вещей, как объектно-ориентированное программирование, где «объекты» объединяют некоторые скрытые данные вместе с некоторыми методами, которые работают со скрытыми данными. Так, например, объект с двумя методами - один, который возвращает, Int
а другой, - String
может быть реализован с этим типом:
myObject :: ∀r.(∀a.(a → Int, a -> String) → a → r) → r
Как это работает? Объект реализован как функция, которая имеет некоторые внутренние данные скрытого типа a
. Чтобы фактически использовать объект, его клиенты передают функцию обратного вызова, которую объект будет вызывать двумя методами. Например:
myObject String (Λa. λ(length, name):(a → Int, a → String). λobjData:a. name objData)
Здесь мы, по сути, вызываем второй метод объекта, тип которого a → String
неизвестен a
. Что ж, неизвестно myObject
клиентам России; но эти клиенты знают из подписи, что они смогут применить к нему любую из двух функций и получить либо a, Int
либо String
.
Для реального примера Haskell ниже приведен код, который я написал, когда учился RankNTypes
. Это реализует тип, называемый, ShowBox
который связывает вместе значение некоторого скрытого типа вместе с его Show
экземпляром класса. Обратите внимание, что в примере внизу я составляю список ShowBox
, первый элемент которого состоит из числа, а второй - из строки. Поскольку типы скрыты с использованием типов более высокого ранга, это не нарушает проверку типов.
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}
type ShowBox = forall b. (forall a. Show a => a -> b) -> b
mkShowBox :: Show a => a -> ShowBox
mkShowBox x = \k -> k x
-- | This is the key function for using a 'ShowBox'. You pass in
-- a function @k@ that will be applied to the contents of the
-- ShowBox. But you don't pick the type of @k@'s argument--the
-- ShowBox does. However, it's restricted to picking a type that
-- implements @Show@, so you know that whatever type it picks, you
-- can use the 'show' function.
runShowBox :: forall b. (forall a. Show a => a -> b) -> ShowBox -> b
-- Expanded type:
--
-- runShowBox
-- :: forall b. (forall a. Show a => a -> b)
-- -> (forall b. (forall a. Show a => a -> b) -> b)
-- -> b
--
runShowBox k box = box k
example :: [ShowBox]
-- example :: [ShowBox] expands to this:
--
-- example :: [forall b. (forall a. Show a => a -> b) -> b]
--
-- Without the annotation the compiler infers the following, which
-- breaks in the definition of 'result' below:
--
-- example :: forall b. [(forall a. Show a => a -> b) -> b]
--
example = [mkShowBox 5, mkShowBox "foo"]
result :: [String]
result = map (runShowBox show) example
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, но я просто остановлюсь на одном моменте, который он не затронул. Требование, чтобы аргумент был полиморфным, не только позволяет использовать его с несколькими типами; он также ограничивает то, что эта функция может делать со своим аргументом (ами) и как она может производить свой результат. То есть это дает вызывающему абоненту меньшую гибкость. Почему вы хотите это сделать? Начну с простого примера:
Допустим, у нас есть тип данных
data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly
и мы хотим написать функцию
f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]
который принимает функцию, которая должна выбрать один из элементов из данного списка и вернуть IO
действие, запускающее ракеты по этой цели. Мы могли бы дать f
простой тип:
f :: ([Country] -> Country) -> IO ()
Проблема в том, что мы могли случайно запустить
f (\_ -> BestAlly)
и тогда у нас будут большие проблемы! Придание f
полиморфному типу ранга 1
f :: ([a] -> a) -> IO ()
совершенно не помогает, потому что мы выбираем тип a
при вызове f
, а просто специализируемся на нем Country
и \_ -> BestAlly
снова используем наш злонамеренный . Решение - использовать тип ранга 2:
f :: (forall a . [a] -> a) -> IO ()
Теперь функция, которую мы передаем, должна быть полиморфной, поэтому \_ -> BestAlly
проверка типа не требуется ! Фактически, ни одна функция, возвращающая элемент, не входящий в данный список, не будет проверять тип (хотя некоторые функции, которые входят в бесконечные циклы или вызывают ошибки и поэтому никогда не возвращаются, будут делать это).
Вышеизложенное, конечно, надумано, но изменение этой техники является ключом к обеспечению ST
безопасности монады.
Типы более высокого ранга не так уж экзотичны, как предполагают другие ответы. Вы не поверите, но многие объектно-ориентированные языки (включая Java и C #!) Имеют их. (Конечно, никто в этих сообществах не знает их под пугающим названием «высокопоставленные типы».)
Например , я собираюсь дать это реализация учебника шаблона Visitor, который я использую все время в моей повседневной работе. Этот ответ не предназначен для ознакомления с шаблоном посетителя; эти знания легко доступны в другом месте .
В этом дурацком воображаемом HR-приложении мы хотим работать с сотрудниками, которые могут быть постоянными сотрудниками на полную ставку или временными подрядчиками. Мой предпочтительный вариант шаблона посетителя (и действительно тот, который имеет отношение к нему RankNTypes
) параметризует тип возвращаемого значения посетителя.
interface IEmployeeVisitor<T>
{
T Visit(PermanentEmployee e);
T Visit(Contractor c);
}
class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }
Дело в том, что несколько посетителей с разными типами возврата могут работать с одними и теми же данными. Это средство не IEmployee
должно выражать мнения о том, что T
должно быть.
interface IEmployee
{
T Accept<T>(IEmployeeVisitor<T> v);
}
class PermanentEmployee : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
class Contractor : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
Хочу обратить ваше внимание на типы. Обратите внимание, что он IEmployeeVisitor
универсально количественно определяет свой возвращаемый тип, тогда как IEmployee
количественно оценивает его внутри своего Accept
метода, то есть на более высоком уровне. Неуклюжий перевод с C # на Haskell:
data IEmployeeVisitor r = IEmployeeVisitor {
visitPermanent :: PermanentEmployee -> r,
visitContractor :: Contractor -> r
}
newtype IEmployee = IEmployee {
accept :: forall r. IEmployeeVisitor r -> r
}
Вот и все. Типы более высокого ранга появляются в C #, когда вы пишете типы, содержащие универсальные методы.
Слайды из курса Брайана О'Салливана по Haskell в Стэнфорде помогли мне понять Rank2Types
.
Для тех, кто знаком с объектно-ориентированными языками, функция более высокого ранга - это просто универсальная функция, которая ожидает в качестве аргумента другую универсальную функцию.
Например, в TypeScript вы можете написать:
type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>
Видите, как универсальный тип функции 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
моем примере вы не можете выпустить . У меня нет справочника, но вы вполне можете найти что-нибудь в "Избавьтесь от классов вашего типа" . Полиморфизм более высокого ранга действительно может вызвать проблемы с проверкой типов, но ограниченная сортировка, неявная в системе классов, - это нормально.