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
.