различия: GADT, семейство данных, семейство данных, которое является GADT

В чем / почему разница между этими тремя? Является ли GADT (и обычные типы данных) сокращением семейства данных? В чем конкретно разница между:

data GADT a  where
  MkGADT :: Int -> GADT Int

data family FGADT a
data instance FGADT a  where             -- note not FGADT Int
  MkFGADT :: Int -> FGADT Int

data family DF a
data instance DF Int  where              -- using GADT syntax, but not a GADT
  MkDF :: Int -> DF Int

(Эти примеры слишком упрощены, поэтому я не вижу тонкостей различий?)

Семейства данных расширяемы, но GADT - нет. Экземпляры семейства данных OTOH не должны перекрываться. Поэтому я не мог объявить другой экземпляр / любые другие конструкторы для FGADT; точно так же, как я не могу объявить другие конструкторы для GADT. Могу объявить другие экземпляры для DF.

При сопоставлении с образцом в этих конструкторах правая часть уравнения «знает», что полезная нагрузка равна Int.

Для экземпляров классов (я был удивлен, обнаружив) я могу написать перекрывающиеся экземпляры для использования GADT:

instance C (GADT a) ...
instance {-# OVERLAPPING #-} C (GADT Int) ...

и аналогично для (FGADT a), (FGADT Int). Но не для (DF a): это должно быть для (DF Int) - это имеет смысл; нет data instance DF a, и если бы он был, он бы перекрывался.

ДОБАВИТЬ: чтобы уточнить ответ @ kabuhr (спасибо)

вопреки тому, что, как я думаю, вы утверждаете в части своего вопроса, для простого семейства данных сопоставление в конструкторе не выполняет никакого вывода

Эти типы сложны, поэтому я полагаю, что для работы с ними мне потребуются явные подписи. В этом случае проще всего использовать простые данные.

inferDF (MkDF x) = x                 -- works without a signature

Выведенный тип inferDF :: DF Int -> Int имеет смысл. Ставить подпись inferDF :: DF a -> a не имеет смысла: для data instance DF a ... нет декларации. Аналогично с foodouble :: Foo Char a -> a нет data instance Foo Char a ....

Я уже знаю, что GADT неудобны. Так что ни один из них не работает без явной подписи

inferGADT (MkGADT x) = x
inferFGADT (MkFGADT x) = x

Как вы говорите, таинственное послание «неприкасаемых». В своем комментарии «сопоставление этих конструкторов» я имел в виду следующее: компилятор «знает» по правой стороне уравнения, что полезная нагрузка равна Int (для всех трех конструкторов), поэтому вам лучше получить любые подписи, соответствующие этому.

Тогда я думаю, что data GADT a where ... это как если бы data instance GADT a where .... Могу поставить подпись inferGADT :: GADT a -> a или inferGADT :: GADT Int -> Int (аналогично для inferFGADT). В этом есть смысл: есть data instance GADT a ... или я могу поставить подпись на более конкретном типе.

Таким образом, в некотором смысле семейства данных являются обобщением GADT. Я тоже вижу как ты говоришь

Таким образом, в некотором смысле GADT являются обобщением семейств данных.

Хм. (Причина вопроса в том, что GHC Haskell дошел до стадии раздутия функций: слишком много похожих, но разных расширений. Я пытался сократить его до меньшего числа базовых абстракций. Затем подход @ HTNW к объяснению с точки зрения дальнейших расширений это противоположно тому, что могло бы помочь учащемуся. Существующие ИМО в типах данных должны быть исключены: вместо этого используйте GADT. PatternSynonyms следует объяснять с точки зрения типов данных и функций сопоставления между ними, а не наоборот. О, и есть кое-что из DataKinds, которое я пропустил при первом чтении.)


person AntC    schedule 17.09.2018    source источник
comment
data family - это не более чем способ сгруппировать несколько объявлений нового типа / данных. Фактически, вы можете почти думать о семействе данных как о семействе инъективных открытых типов (где каждый data instance соответствует объявлению данных, а type instance - этому объявлению). Эта аналогия распадается в двух местах (которые я могу придумать): конструкторы типов семейств данных могут быть частично применены, а конструкторы типов семейств данных имеют более строгие правила ввода (MyDataFamily a ~ g b подразумевает MyDataFamily ~ g и a ~ b, а MyInjectiveTyFam a ~ MyInjectiveTyFam b подразумевает a ~ b).   -  person Alec    schedule 17.09.2018


Ответы (2)


Для начала вы должны думать о семействе данных как о совокупности независимых ADT, которые индексируются типом, в то время как GADT - это единственный тип данных с параметром выводимого типа, где ограничения на этот параметр (обычно, ограничения равенства, такие как a ~ Int) можно включить в область действия путем сопоставления с образцом.

Это означает, что самая большая разница заключается в том, что, вопреки тому, что, как я думаю, вы утверждаете в части своего вопроса, для простого семейства данных сопоставление в конструкторе не выполняет никаких выводов для параметра типа . В частности, это проверка типов:

inferGADT :: GADT a -> a
inferGADT (MkGADT n) = n

но это не:

inferDF :: DF a -> a
inferDF (MkDF n) = n

и без сигнатур типа первый не сможет выполнить проверку типа (с загадочным сообщением «неприкасаемый»), а второй будет выведен как DF Int -> Int.

Ситуация становится немного более запутанной для чего-то вроде вашего FGADT типа, который объединяет семейства данных с GADT, и, признаюсь, я особо не думал о том, как это работает в деталях. Но в качестве интересного примера рассмотрим:

data family Foo a b
data instance Foo Int a where
  Bar :: Double -> Foo Int Double
  Baz :: String -> Foo Int String
data instance Foo Char Double where
  Quux :: Double -> Foo Char Double
data instance Foo Char String where
  Zlorf :: String -> Foo Char String

В этом случае Foo Int a - это GADT с выводимым параметром a:

fooint :: Foo Int a -> a
fooint (Bar x) = x + 1.0
fooint (Baz x) = x ++ "ish"

но Foo Char a - это просто набор отдельных ADT, поэтому проверка типов не будет:

foodouble :: Foo Char a -> a
foodouble (Quux x) = x

по той же причине inferDF не будет проверять тип выше.

Теперь, возвращаясь к вашим простым типам DF и GADT, вы можете в значительной степени имитировать DFs, просто используя GADTs. Например, если у вас есть DF:

data family MyDF a
data instance MyDF Int where
  IntLit :: Int -> MyDF Int
  IntAdd :: MyDF Int -> MyDF Int -> MyDF Int
data instance MyDF Bool where
  Positive :: MyDF Int -> MyDF Bool

вы можете написать его как GADT, просто написав отдельные блоки конструкторов:

data MyGADT a where
  -- MyGADT Int
  IntLit' :: Int -> MyGADT Int
  IntAdd' :: MyGADT Int -> MyGADT Int -> MyGADT Int
  -- MyGADT Bool
  Positive' :: MyGADT Int -> MyGADT Bool

Таким образом, в некотором смысле GADT являются обобщением семейств данных. Однако основным вариантом использования семейств данных является определение связанных типов данных для классов:

class MyClass a where
  data family MyRep a
instance MyClass Int where
  data instance MyRep Int = ...
instance MyClass String where
  data instance MyRep String = ...

где необходим «открытый» характер семейств данных (и где основанные на шаблонах методы вывода GADT бесполезны).

person K. A. Buhr    schedule 17.09.2018
comment
вы можете в значительной степени имитировать DF, просто используя GADT, я считаю вводящим в заблуждение, потому что у вас может быть экземпляр DF с новым типом stackoverflow.com/questions/52417415/; эмуляция этого как GADT заблокировала бы его от нового типа. Я бы сказал, что GADT верхнего уровня похож на DF с одним экземпляром, в котором заголовок экземпляра - DF a. - person AntC; 20.09.2018

Я думаю, разница станет очевидной, если мы будем использовать сигнатуры типа PatternSynonyms для конструкторов данных. Начнем с Haskell 98.

data D a = D a a

Получается тип выкройки:

pattern D :: forall a. a -> a -> D a

его можно читать в двух направлениях. D в контексте "вперед" или выражения говорит: "forall a, вы можете дать мне 2 a, и я дам вам D a". «Назад», как образец, он говорит: «forall a, вы можете дать мне D a, и я дам вам 2 a».

То, что вы пишете в определении GADT, не являются типами шаблонов. Кто они такие? Ложь. Ложь, ложь, ложь. Уделите им внимание только постольку, поскольку в качестве альтернативы можно написать их вручную с помощью ExistentialQuantification. Давай воспользуемся этим

data GD a where
  GD :: Int -> GD Int

Ты получаешь

--                      vv ignore
pattern GD :: forall a. () => (a ~ Int) => Int -> GD a

Здесь сказано: forall a, вы можете дать мне GD a, и я могу предоставить вам доказательство того, что a ~ Int плюс Int.

Важное замечание: тип возврата / соответствия конструктора GADT всегда - "заголовок типа данных". Я определил data GD a where ...; Я получил GD :: forall a. ... GD a. Это также верно для конструкторов Haskell 98, а также data family конструкторов, хотя это немного более тонко.

Если у меня есть GD a, и я не знаю, что такое a, я все равно могу перейти в GD, хотя я написал GD :: Int -> GD Int, что, кажется, говорит, что я могу сопоставить его только с GD Ints. Вот почему я говорю, что конструкторы GADT лгут. Тип паттерна никогда не лжет. В нем четко указано, что forall a, я могу сопоставить GD a с конструктором GD и получить свидетельство для a ~ Int и значения Int.

Хорошо, data familys. Не будем пока смешивать их с GADT.

data Nat = Z | S Nat
data Vect (n :: Nat) (a :: Type) :: Type where
  VNil :: Vect Z a
  VCons :: a -> Vect n a -> Vect (S n) a -- try finding the pattern types for these btw

data family Rect (ns :: [Nat]) (a :: Type) :: Type
newtype instance Rect '[] a = RectNil a
newtype instance Rect (n : ns) a = RectCons (Vect n (Rect ns a))

На самом деле сейчас есть две головки типа данных. Как говорит @KA.Buhr, разные data instance действуют как разные типы данных, которые просто случаются совместно использовать имя. Типы паттернов:

pattern RectNil :: forall a. a -> Rect '[] a
pattern RectCons :: forall n ns a. Vect n (Rect ns a) -> Rect (n : ns) a

Если у меня Rect ns a, и я не знаю, что такое ns, я не могу сопоставить его. RectNil занимает только Rect '[] a с, RectCons занимает только Rect (n : ns) a. Вы можете спросить: «Зачем мне снижение мощности?» @KABuhr дал одно: GADT закрыты (и не зря; следите за обновлениями), семьи открыты. В случае Rect этого не происходит, поскольку эти экземпляры уже заполняют все пространство [Nat] * Type. Причина на самом деле newtype.

Вот GADT RectG:

data RectG :: [Nat] -> Type -> Type where
  RectGN :: a -> RectG '[] a
  RectGC :: Vect n (RectG ns a) -> RectG (n : ns) a

я получил

-- it's fine if you don't get these
pattern RectGN :: forall ns a. () => (ns ~ '[]) => a -> RectG ns a
pattern RectGC :: forall ns' a. forall n ns. (ns' ~ (n : ns)) =>
                  Vect n (RectG ns a) -> RectG ns' a
-- just note that they both have the same matched type
-- which means there needs to be a runtime distinguishment 

Если у меня есть RectG ns a, и я не знаю, что такое ns, я все равно могу сопоставить его. Компилятор должен сохранить эту информацию с помощью конструктора данных. Итак, если бы у меня был RectG [1000, 1000] Int, я бы понес накладные расходы в размере одного миллиона RectGN конструкторов, которые все «сохраняют» одну и ту же «информацию». Однако Rect [1000, 1000] Int в порядке, поскольку у меня нет возможности сопоставить и определить, является ли Rect RectNil или RectCons. Это позволяет конструктору быть newtype, поскольку он не содержит информации. Вместо этого я бы использовал другой GADT, что-то вроде

data SingListNat :: [Nat] -> Type where
  SLNN :: SingListNat '[]
  SLNCZ :: SingListNat ns -> SingListNat (Z : ns)
  SLNCS :: SingListNat (n : ns) -> SingListNat (S n : ns)

который хранит размеры Rect в пространстве O(sum ns) вместо O(product ns) (я думаю, что это правильно). Вот почему GADT закрыты, а семьи открыты. GADT похож на обычный тип data, за исключением того, что он имеет свидетельство равенства и экзистенциальность. Добавлять конструкторы в GADT не имеет смысла больше, чем есть смысл добавлять конструкторы в тип Haskell 98, потому что любой код, который не знает об одном из конструкторов, относится к very плохое время. Однако это нормально для семей, потому что, как вы заметили, после того, как вы определите ветвь семейства, вы не сможете добавить больше конструкторов в эту ветвь. Как только вы узнаете, в какой ветке вы находитесь, вы узнаете конструкторы, и никто не сможет это сломать. Вам не разрешается использовать какие-либо конструкторы, если вы не знаете, какую ветвь использовать.

Ваши примеры на самом деле не смешивают GADT и семейства данных. Типы шаблонов хороши тем, что они нормализуют поверхностные различия в data определениях, так что давайте посмотрим.

data family FGADT a
data instance FGADT a where
  MkFGADT :: Int -> FGADT Int

Дает тебе

pattern MkFGADT :: forall a. () => (a ~ Int) => Int -> FGADT a
-- no different from a GADT; data family does nothing

Но

data family DF a
data instance DF Int where
  MkDF :: Int -> DF Int

дает

pattern MkDF :: Int -> DF Int
-- GADT syntax did nothing

Вот правильное сведение

data family Map k :: Type -> Type
data instance Map Word8 :: Type -> Type where
  MW8BitSet :: BitSet8 -> Map Word8 Bool
  MW8General :: GeneralMap Word8 a -> Map Word8 a

Что дает шаблоны

pattern MW8BitSet :: forall a. () => (a ~ Bool) => BitSet8 -> Map Word8 a
pattern MW8General :: forall a. GeneralMap Word8 a -> Map Word8 a

Если у меня есть Map k v, и я не знаю, что такое k, я не могу сопоставить его с MW8General или MW8BitSet, потому что им нужны только Map Word8. Это влияние data family. Если у меня есть Map Word8 v, и я не знаю, что такое v, сопоставление конструкторов может показать мне, известен ли он как Bool или что-то еще.

person HTNW    schedule 17.09.2018
comment
Это очень полезно! Надеюсь, вы не возражаете, но я добавил примечание о синтаксисе forall a. forall s. Num s=> ..., соответствующем () => Num s => ..., поскольку я никогда раньше не осознавал этого и предполагаю, что я не единственный. - person K. A. Buhr; 17.09.2018
comment
Почему ты врешь? - person dfeuer; 17.09.2018
comment
Возможно, не откровенная ложь, но определенно ложь по бездействию. Типы конструкторов определения GADT точны только при использовании в качестве функций. Если не учитывать типы паттернов, кажется, что data D a where D :: Int -> D Int и data family D a; data instance D Int = D Int должны вести себя одинаково: D :: Int -> D Int. Но это заблуждение; правда pattern D :: forall a. () => (a ~ Int) => Int -> D a и pattern D :: Int -> D Int и это не одно и то же. - person HTNW; 17.09.2018
comment
Проголосовали против: объяснение двух сложных концепций с точки зрения еще двух (сигнатуры паттернов и экзистенциальный квант), а затем еще одного (одиночные) - это противоположность полезности. Хотел бы я не читать твой ответ. Теперь я запутался больше. - person AntC; 18.09.2018
comment
@AntC это лучше? - person HTNW; 18.09.2018
comment
Спасибо за попытку. Я чувствую, что вы даете мне свалку всего, что вы знаете о децентрализации данных. Это хорошо, если другие понимают: на SO не так много по этой теме; Документы GHC, как известно, описывают каждое расширение отдельно и плохо объясняют, как они взаимодействуют или мешают. Что касается меня, вы, кажется, ответили на множество вопросов, которые я не задавал и к которым не готов. Было бы обидно, если бы эта информация была похоронена / ее было бы трудно найти; Я предлагаю вам разбить его на отдельные ответы и публиковать ТАК каждый вопрос, который, по вашему мнению, вы решаете, и свой ответ. - person AntC; 18.09.2018