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
.
AnyType
тип ранга 2; это просто сбивает с толку, и я удалил его. КонструкторAnyType
действует как функция ранга 2, а конструкторSomeType
действует как функция ранга 1 (так же, как большинство несуществующих типов), но это не очень полезная характеристика. Во всяком случае, то, что делает эти типы интересными, так это то, что они сами имеют ранг 0 (т. Е. Не определены количественно по переменной типа и поэтому мономорфны), даже если они «содержат» количественные типы.Поскольку
Worker
, как определено, принимает только один аргумент, тип поля ввода (переменная типаx
). НапримерWorker Int
, это тип. Переменная типаb
, напротив, не является параметромWorker
, а является своего рода «локальной переменной», так сказать. Это не может быть передано как вWorker Int String
- это вызвало бы ошибку типа.Если мы вместо этого определили:
тогда
Worker Int String
будет работать, но тип больше не является экзистенциальным - теперь мы всегда должны также передавать тип буфера.Это примерно правильно. Вкратце, каждый раз, когда вы применяете конструктор
Worker
, GHC выводитb
тип из аргументовWorker
, а затем ищет экземплярBuffer b
. Если это найдено, GHC включает дополнительный указатель на экземпляр в объекте. В своей простейшей форме это не слишком отличается от «указателя на vtable», который добавляется к каждому объекту в ООП при наличии виртуальных функций.В общем случае все может быть гораздо сложнее. Компилятор может использовать другое представление и добавить больше указателей вместо одного (скажем, непосредственное добавление указателей ко всем методам экземпляра), если это ускоряет код. Кроме того, иногда компилятору необходимо использовать несколько экземпляров для удовлетворения ограничения. Например, если нам нужно сохранить экземпляр для
Eq [Int]
... тогда существует не один, а два: один дляInt
и один для списков, и эти два нужно объединить (во время выполнения, за исключением оптимизации).Трудно догадаться, что именно делает GHC в каждом случае: это зависит от тонны оптимизаций, которые могут или не могут сработать.
Вы можете попробовать поискать в «словарной» реализации классов типов, чтобы узнать больше о том, что происходит. Вы также можете попросить GHC напечатать внутреннее оптимизированное Ядро
-ddump-simpl
и наблюдать, как создаются, хранятся и передаются словари. Я должен предупредить вас: ядро довольно низкого уровня, и поначалу его трудно прочитать.источник