Роз'яснення екзистенціальних типів у Хаскеллі


10

Я намагаюся зрозуміти екзистенційні типи в Haskell і натрапив на PDF http://www.ii.uni.wroc.pl/~dabi/courses/ZPF15/rlasocha/prezentacja.pdf

Будь ласка, виправте мої нижче розуміння, які я маю дотепер.

  • Екзистенціальні типи, схоже, не цікавляться типом, який вони містять, але узор, що відповідає їм, говорить про те, що існує якийсь тип, який ми не знаємо, що це до тих пір, поки ми не використовуємо Typeable або Data.
  • Ми використовуємо їх, коли хочемо приховати типи (наприклад: для гетерогенних списків) або насправді не знаємо, які типи є в час компіляції.
  • GADTзабезпечити чіткий і кращий синтаксис коду за допомогою екзистенціальних типів, надаючи неявні forall's

Мої сумніви

  • У Сторінці 20 вище PDF для коду нижче зазначено, що функція не може вимагати конкретного буфера. Чому так? Коли я розробляю функцію, я точно знаю, який тип буфера я буду використовувати щонайбільше, хоча я не можу знати, які дані я буду вносити до цього. Що не так у наявності :: Worker MemoryBuffer IntЯкщо вони дійсно хочуть абстрагуватися над буфером, вони можуть мати тип суми data Buffer = MemoryBuffer | NetBuffer | RandomBufferта мати такий тип:: Worker Buffer Int
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int
  • Оскільки Haskell - це мова для стирання повного типу, як C, то як вона знає під час виконання, яку функцію викликати. Це щось на кшталт того, що ми збережемо небагато інформації та передамо у величезну V-таблицю функцій, а під час виконання вона з’ясується з V-Table? Якщо так, то яку саме інформацію вона зберігатиме?

Відповіді:


8

GADT надають чіткий та кращий синтаксис коду за допомогою Existential Types, надаючи неявні forall

Я думаю, що існує загальна згода, що синтаксис GADT краще. Я б не сказав, що це тому, що GADT надають неявні записки, а скоріше тому, що оригінальний синтаксис, включений із ExistentialQuantificationрозширенням, потенційно плутає / вводить в оману. Цей синтаксис, звичайно, виглядає так:

data SomeType = forall a. SomeType a

або з обмеженням:

data SomeShowableType = forall a. Show a => SomeShowableType a

і я думаю, що консенсус полягає в тому, що використання ключового слова forallтут дозволяє легко сплутати тип із абсолютно іншим типом:

data AnyType = AnyType (forall a. a)    -- need RankNTypes extension

Кращий синтаксис, можливо, використовував окреме existsключове слово, тож ви напишете:

data SomeType = SomeType (exists a. a)   -- not valid GHC syntax

Синтаксис GADT, незалежно від того, чи використовується імпліцитно чи явно forall, є більш рівномірним для цих типів, і, здається, його легше зрозуміти. Навіть із чітким forallвизначенням наступне визначення поширюється на думку про те, що ви можете взяти значення будь-якого типу aі помістити його всередині мономорфного SomeType':

data SomeType' where
    SomeType' :: forall a. (a -> SomeType')   -- parentheses optional

і легко помітити і зрозуміти різницю між цим типом і:

data AnyType' where
    AnyType' :: (forall a. a) -> AnyType'

Екзистенціальні типи, схоже, не цікавляться типом, який вони містять, але узор, що відповідає їм, говорить про те, що існує якийсь тип, який ми не знаємо, що це до тих пір, поки ми не використовуємо Typeable або Data.

Ми використовуємо їх, коли хочемо приховати типи (наприклад: для гетерогенних списків) або насправді не знаємо, які типи є в час компіляції.

Я думаю, це не надто далеко, хоча вам не доведеться використовувати Typeableабо Dataвикористовувати екзистенційні типи. Думаю, було б більш точно сказати, що екзистенційний тип забезпечує добре набрану "коробку" навколо не визначеного типу. Поле справді "приховує" тип у певному сенсі, що дозволяє скласти неоднорідний список таких полів, ігноруючи типи, які вони містять. Виявляється, нестримне екзистенційне, як SomeType'вище, досить марне, але обмежене:

data SomeShowableType' where
    SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'

дозволяє вам відповідати шаблону, щоб зазирнути всередину "коробки" та зробити доступними засоби класу типу:

showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x

Зауважте, що це працює для будь-якого класу типів, а не лише Typeableабо Data.

Що стосується вашої плутанини щодо сторінки 20 слайд-колоди, автор каже, що функція, яка приймає екзистенціал, Worker вимагати Workerконкретного Bufferекземпляра неможлива . Ви можете написати функцію для створення Workerпевного типу Buffer, наприклад MemoryBuffer:

class Buffer b where
  output :: String -> b -> IO ()
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer
instance Buffer MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int

але якщо ви пишете функцію, яка бере Workerаргумент, вона може використовувати лише загальні Bufferзасоби класу типу (наприклад, функцію output):

doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b

Він не може намагатися вимагати, щоб bце був певний тип буфера, навіть через відповідність шаблонів:

doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
  MemoryBuffer -> error "try this"       -- type error
  _            -> error "try that"

Нарешті, інформація про час існування про екзистенціальні типи стає доступною через неявні аргументи «словника» для типових класів. WorkerТипу вище, в addtion до наявності поля для буфера і входу, а також має невидиму неявне поле , яке вказує на Bufferсловник (кілька , як у-таблиці, хоча це навряд чи величезний, так як він просто містить покажчик на відповідну outputфункцію).

Всередині клас типу Bufferпредставлений у вигляді типу даних із функціональними полями, а екземпляри є "словниками" цього типу:

data Buffer' b = Buffer' { output' :: String -> b -> IO () }

dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }

Екзистенційний тип має приховане поле для цього словника:

data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }

а така функція, doWorkяка діє на екзистенційні Worker'значення, реалізується як:

doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b

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


Чи існують екзистенціали як ранг 1 для декларацій даних? Екзистенціалі - це спосіб розібратися з віртуальними функціями в Haskell, як і в будь-якій мові OOP?
Pawan Kumar

1
Напевно, я не повинен був би називати AnyTypeтип 2; це просто заплутано, і я його видалив. Конструктор AnyTypeдіє як функція рангу 2, а конструктор SomeTypeдіє функцію рангу 1 (так само , як і більшість , не є -existential типів), але це не дуже корисно характеристики. У будь-якому випадку, ці типи цікаві, це те, що вони займають ранг 0 (тобто не визначаються кількісно за змінною типу і настільки мономорфні), хоча вони "містять" кількісно визначені типи.
KA Buhr

1
Класи типів (а саме їх функції методу), а не екзистенціальні типи, ймовірно, є найбільш прямим еквівалентним Haskell віртуальним функціям. У технічному сенсі класи та об'єкти мов OOP можна розглядати як екзистенційні типи та значення, але практично часто існують кращі способи реалізації стилю поліморфізму OOP "віртуальної функції" в Haskell, ніж екзистенціалі, такі як типи суми, типи класів та / або параметричний поліморфізм.
KA Buhr

4

У Сторінці 20 вище PDF для коду нижче зазначено, що функція не може вимагати конкретного буфера. Чому так?

Оскільки Worker, як визначено, береться лише один аргумент, тип поля "введення" (тип змінної x). Напр. Worker Int- тип. Змінна типу b, натомість, не є параметром Worker, але є свого роду "локальною змінною", так би мовити. Він не може бути переданий як в Worker Int String- це призведе до помилки типу.

Якщо ми натомість визначили:

data Worker x b = Worker {buffer :: b, input :: x}

тоді він Worker Int Stringби працював, але тип більше не існує, тому ми завжди мусимо також передавати тип буфера.

Оскільки Haskell - це мова для стирання повного типу, як C, то як вона знає під час виконання, яку функцію викликати. Це щось на кшталт того, що ми збережемо небагато інформації та передамо у величезну V-таблицю функцій, а під час виконання вона з’ясується з V-Table? Якщо так, то яку саме інформацію вона зберігатиме?

Це приблизно правильно. Коротко кажучи, кожен раз, коли ви застосовуєте конструктор Worker, GHC виводить bтип з аргументів Worker, а потім здійснює пошук примірника Buffer b. Якщо це буде знайдено, GHC включає додатковий вказівник на екземпляр в об'єкті. У своїй найпростішій формі це не надто відрізняється від "покажчика на vtable", який додається до кожного об'єкту в OOP, коли віртуальних функцій немає.

У загальному випадку він може бути набагато складнішим. Компілятор може використовувати інше представлення і додавати більше покажчиків замість одного (скажімо, безпосередньо додаючи покажчики до всіх методів екземпляра), якщо це прискорить код. Крім того, іноді компілятору потрібно використовувати кілька примірників, щоб задовольнити обмеження. Наприклад, якщо нам потрібно зберігати екземпляр для Eq [Int]..., то існує не один, а два: один для Intта один для списків, а два потрібно поєднувати (під час виконання, оптимізація заборони).

Важко здогадатися, що саме робить GHC у кожному конкретному випадку: це залежить від тонни оптимізацій, які можуть або не можуть спрацювати.

Ви можете спробувати googling для "словника" реалізації типів класів, щоб дізнатися більше про те, що відбувається. Ви також можете попросити GHC роздрукувати внутрішній оптимізований Core -ddump-simplта спостерігати за тим, як словники будуються, зберігаються та передаються навколо. Я маю попередити вас: Core досить низький рівень, і його спочатку важко читати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.