Я не дуже досвідчений в 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
. Без ran2types немає можливості, щоб функція вимагала, щоб її аргумент був поліморфною функцією, і, таким чином, також немає можливості використовувати його як поліморфну функцію.
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)
означає "для всіх х, х - собака". "Λ" - це велика лямбда, яка використовується для абстрагування параметрів типу; що другий рядок говорить, що id - це функція, яка приймає тип t
, а потім повертає функцію, параметризовану цим типом.
Розумієте, у Системі F ви не можете просто застосувати подібну функцію id
до значення одразу; спочатку вам потрібно застосувати функцію Λ до типу, щоб отримати λ-функцію, яку ви застосуєте до значення. Так, наприклад:
(Λt.λx: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
, я вважаю, що причина полягає в тому, що він використовує таку техніку за кадром.
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 #!) Містять їх. (Зрозуміло, ніхто в цих громадах не знає їх за страхітливою назвою "типи вищого рангу".)
Приклад, який я надам, - це реалізація в підручнику схеми відвідувачів, яку я весь час використовую у своїй щоденній роботі. Ця відповідь не задумана як ознайомлення із схемою відвідувачів; що знання легко доступні в інших місцях .
У цій товстій уявній 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 дозволяє вийти з обмеженим поліморфізмом 2-го рангу, використовуючи подібні класи.
forall
мого прикладу. Я не маю довідки, але ви, можливо, знайдете щось у "Запишіть свої класи" . Поліморфізм вищого рангу дійсно може запровадити проблеми перевірки типу, але обмежений різновид, що міститься в системі класів, є нормальним.