Какова цель Rank2Types?


110

Я не очень разбираюсь в Haskell, поэтому это может быть очень простой вопрос.

Какие языковые ограничения снимает Rank2Types ? Разве функции в Haskell не поддерживают полиморфные аргументы?


По сути, это обновление системы типов HM до полиморфного лямбда-исчисления, также известного как. λ2 / Система F. Имейте в виду, что вывод типа неразрешим в λ2.
Poscat

Ответы:


116

Разве функции в Haskell не поддерживают полиморфные аргументы?

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

Например, следующую функцию нельзя ввести без этого расширения, потому что gона используется с разными типами аргументов в определении f:

f g = g 1 + g "lala"

Обратите внимание, что вполне возможно передать полиморфную функцию в качестве аргумента другой функции. Так что что-то вроде map id ["a","b","c"]совершенно законно. Но функция может использовать его только как мономорфный. В примере mapиспользуется, idкак если бы он имел тип String -> String. И, конечно же, вы также можете передать простую мономорфную функцию данного типа вместо id. Без rank2types функция не может требовать, чтобы ее аргумент был полиморфной функцией, и, следовательно, не может использовать его как полиморфную функцию.


5
Чтобы добавить несколько слов, связывающих мой ответ с этим: рассмотрим функцию Haskell 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?
Луис Касильяс

166

Трудно понять полиморфизм более высокого ранга, если вы не изучаете Систему F напрямую, потому что Haskell спроектирован так, чтобы скрыть детали этого от вас в интересах простоты.

Но, по сути, грубая идея состоит в том, что полиморфные типы на самом деле не имеют той a -> bформы, которую они имеют в Haskell; на самом деле они выглядят так, всегда с явными квантификаторами:

id :: a.a  a
id = Λtx:t.x

Если вы не знаете символа «∀», он читается как «для всех»; ∀x.dog(x)означает «для всех x, x - собака». «Λ» - заглавная лямбда, используемая для абстрагирования по параметрам типа; во второй строке говорится, что id - это функция, которая принимает тип t, а затем возвращает функцию, параметризованную этим типом.

Видите ли, в системе F вы не можете сразу применить такую ​​функцию idк значению; сначала вам нужно применить Λ-функцию к типу, чтобы получить λ-функцию, которую вы применяете к значению. Так например:

tx: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: для всех, кто читает это, кто задается вопросом, почему ExistentialTypesGHC использует forall, я считаю, что причина в том, что он использует такого рода технику за кулисами.


2
Спасибо за очень подробный ответ! (что, кстати, также окончательно побудило меня изучить правильную теорию типов и Систему F.)
Александр Димитров

5
Если бы у вас было 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, являются просто обычными типами данных, которые также содержат все необходимые словари классов типов (извините, я не смог найти ссылку, подтверждающую это).
Витус

2
@Vitus: экзистенциальные данные 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.
Луис Касильяс

Это отличный ответ, на который мне придется потратить некоторое время. Думаю, я слишком привык к абстракциям, предоставляемым универсальными шаблонами C #, поэтому я принимал многое из этого как должное, вместо того чтобы понимать теорию.
Андрей Щекин

@sacundim: Ну, "все необходимые словари классов типов" также могут означать отсутствие словарей вообще, если они вам не нужны. :) Моя точка зрения заключалась в том, что GHC, скорее всего, не кодирует экзистенциальные типы через типы с более высоким рейтингом (то есть предлагаемое вами преобразование - ∃xP (x) ~ ∀r. (∀xP (x) → r) → r).
Витус

47

Ответ Луиса Касильяса дает много полезной информации о том, что означают типы ранга 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безопасности монады.


18

Типы более высокого ранга не так уж экзотичны, как предполагают другие ответы. Вы не поверите, но многие объектно-ориентированные языки (включая 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 #, когда вы пишете типы, содержащие универсальные методы.


1
Мне было бы интересно узнать, писал ли кто-нибудь еще о поддержке C # / Java / Blub типов более высокого ранга. Если вы, уважаемый читатель, знаете о подобных ресурсах, пришлите их мне!
Бенджамин Ходжсон


-2

Для тех, кто знаком с объектно-ориентированными языками, функция более высокого ранга - это просто универсальная функция, которая ожидает в качестве аргумента другую универсальную функцию.

Например, в 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функцию более высокого ранга.


Что это добавляет к ответу sepp2k?
dfeuer

Или Бенджамина Ходжсона, если на то пошло?
dfeuer

1
Я думаю, вы упустили точку зрения Ходжсона. Acceptимеет полиморфный тип ранга 1, но это метод IEmployee, который сам является рангом 2. Если кто-то даст мне IEmployee, я могу открыть его и использовать его Acceptметод в любом типе.
dfeuer

1
Ваш пример также имеет ранг-2 по Visiteeклассу, который вы представляете. По f :: Visitee e => T eсути, функция (после обессахаривания материала класса) f :: (forall r. e -> Visitor e r -> r) -> T e. Haskell 2010 позволяет вам обойтись ограниченным полиморфизмом второго ранга, используя подобные классы.
dfeuer

1
В forallмоем примере вы не можете выпустить . У меня нет справочника, но вы вполне можете найти что-нибудь в "Избавьтесь от классов вашего типа" . Полиморфизм более высокого ранга действительно может вызвать проблемы с проверкой типов, но ограниченная сортировка, неявная в системе классов, - это нормально.
dfeuer
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.