Яка мета Rank2Types?


110

Я не дуже досвідчений в Haskell, тому це може бути дуже легким питанням.

Які обмеження мови вирішують Rank2Types ? Невже функції Haskell вже не підтримують поліморфні аргументи?


Це в основному оновлення від системи типу HM до поліморфного обчислення лямбда ака. λ2 / Система F. Майте на увазі, що умовиводи не можна визначити у λ2.
Поскат

Відповіді:


116

Чи не функціонують Haskell вже підтримують поліморфні аргументи?

Вони є, але лише ранг 1. Це означає, що, хоча ви можете написати функцію, яка приймає різні типи аргументів без цього розширення, ви не можете записати функцію, яка використовує її аргумент як різні типи в одному виклику.

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

f g = g 1 + g "lala"

Зауважте, що цілком можливо передати поліморфну ​​функцію як аргумент іншій функції. Тож щось подібне map id ["a","b","c"]цілком законно. Але функція може використовувати її лише як мономорфну. У прикладі mapвикористовується idтак, ніби він мав тип String -> String. І звичайно, ви також можете передавати просту мономорфну ​​функцію даного типу замість id. Без ran2types немає можливості, щоб функція вимагала, щоб її аргумент був поліморфною функцією, і, таким чином, також немає можливості використовувати його як поліморфну ​​функцію.


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)означає "для всіх х, х - собака". "Λ" - це велика лямбда, яка використовується для абстрагування параметрів типу; що другий рядок говорить, що id - це функція, яка приймає тип t, а потім повертає функцію, параметризовану цим типом.

Розумієте, у Системі F ви не можете просто застосувати подібну функцію idдо значення одразу; спочатку вам потрібно застосувати функцію Λ до типу, щоб отримати λ-функцію, яку ви застосуєте до значення. Так, наприклад:

tx:t.x) Int 5 = x:Int.x) 5
                  = 5

Стандартний Haskell (тобто Haskell 98 і 2010) спрощує це для вас, не маючи жодного з цих типів кількісних показників, великих лямбда і застосувань типів, але поза кадром GHC вносить їх, коли аналізує програму на компіляцію. (Я вважаю, що це весь час збирання, без реальних витрат.)

Але автоматичне поводження з Хаскеллом означає, що воно передбачає, що "∀" ніколи не з'являється на лівій гілці функціонального типу ("→"). Rank2Typesі RankNTypesвимкніть ці обмеження і дозволять вам замінити правила за замовчуванням Haskell на те, куди потрібно вставити forall.

Чому б ти хотів це зробити? Оскільки повна, необмежена система F є приємною, і вона може робити багато цікавих речей. Наприклад, приховування типу та модульність можна реалізувати, використовуючи типи вищого рангу. Візьмемо для прикладу просту функцію старого типу 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' . Без типів вищого рангу, навпаки, абонент завжди вибирає типи.

Для чого це корисно? Ну, для багатьох речей насправді, але одна ідея полягає в тому, що ви можете використовувати це для моделювання таких речей, як об’єктно-орієнтоване програмування, де "об'єкти" об'єднують деякі приховані дані разом з деякими методами, які працюють на прихованих даних. Так, наприклад, об’єкт з двома методами - одним, який повертає a, Intа іншим, який повертає a 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або a 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, я вважаю, що причина полягає в тому, що він використовує таку техніку за кадром.


2
Дякую за дуже детальну відповідь! (що, до речі, також остаточно мотивувало мене вивчити теорію власного типу та System 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 #!) Містять їх. (Зрозуміло, ніхто в цих громадах не знає їх за страхітливою назвою "типи вищого рангу".)

Приклад, який я надам, - це реалізація в підручнику схеми відвідувачів, яку я весь час використовую у своїй щоденній роботі. Ця відповідь не задумана як ознайомлення із схемою відвідувачів; що знання легко доступні в інших місцях .

У цій товстій уявній 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 дозволяє вийти з обмеженим поліморфізмом 2-го рангу, використовуючи подібні класи.
dfeuer

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