GADT обеспечивают ясный и лучший синтаксис для кода с использованием экзистенциальных типов, предоставляя неявные поля
Я думаю, что есть общее согласие, что синтаксис 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
.