Не знаете, что такое GADT и распространяемые ограничения.

Существует множество вопросов и ответов о том, что GADTs лучше, чем DatatypeContexts, потому что GADT автоматически делают ограничения доступными в нужных местах. Например, здесь, здесь, здесь. Но иногда кажется, что мне все еще нужно явное ограничение. В чем дело? Пример адаптирован из этого ответа:

{-# LANGUAGE  GADTs  #-}
import Data.Maybe                                              -- fromJust

data GADTBag a  where
  MkGADTBag :: Eq a => { unGADTBag :: [a] } -> GADTBag a

baz (MkGADTBag x) (Just y) = x == y

baz2           x        y  = unGADTBag x == fromJust y


-- unGADTBag :: GADTBag a -> [a]                               -- inferred, no Eq a

-- baz ::          GADTBag a -> Maybe [a] -> Bool              -- inferred, no Eq a
-- baz2 :: Eq a => GADTBag a -> Maybe [a] -> Bool              -- inferred, with Eq a

Почему тип для unGADTBag не может сообщить нам Eq a?

baz и baz2 морально эквивалентны, но имеют разные типы. Предположительно из-за того, что unGADTBag не имеет Eq a, ограничение не может распространяться на какой-либо код, использующий unGADTBag.

Но с baz2 внутри GADTBag a прячется ограничение Eq a. Предположительно Eq a baz2 захочет, чтобы дубликат словаря уже был там (?)

Может быть, у GADT может быть много конструкторов данных, каждый с разными (или без) ограничениями? Это не так здесь или с типичными примерами ограниченных структур данных, таких как сумки, наборы, упорядоченные списки.

Эквивалент для типа данных GADTBag с использованием DatatypeContexts подразумевает, что тип baz совпадает с baz2.

Бонусный вопрос: почему я не могу получить обычный ... deriving (Eq) за GADTBag? Я могу получить один с StandaloneDeriving, но это совершенно очевидно, почему GHC не может сделать это за меня?

deriving instance (Eq a) => Eq (GADTBag a)

Проблема снова в том, что могут быть другие конструкторы данных?

(Код изучается в GHC 8.6.5, если это актуально.)

Дополнение: в свете ответов @ chi и @ leftroundabout - ни один из них я не считаю убедительным. Все это дает *** Exception: Prelude.undefined:

*DTContexts> unGADTBag undefined
*DTContexts> unGADTBag $ MkGADTBag undefined
*DTContexts> unGADTBag $ MkGADTBag (undefined :: String)
*DTContexts> unGADTBag $ MkGADTBag (undefined :: [a])

*DTContexts> baz undefined (Just "hello")
*DTContexts> baz (MkGADTBag undefined) (Just "hello")
*DTContexts> baz (MkGADTBag (undefined :: String)) (Just "hello")

*DTContexts> baz2 undefined (Just "hello")
*DTContexts> baz2 (MkGADTBag undefined) (Just "hello")
*DTContexts> baz2 (MkGADTBag (undefined :: String)) (Just "hello")

Принимая во внимание, что эти два выдают ошибку одного и того же типа во время компиляции * Couldn't match expected type ``[Char]' _27 _ / _ 28_ соответственно [Изменить: мой первоначальный Addit дал неправильное выражение и неправильное сообщение об ошибке]:

*DTContexts> baz (MkGADTBag (undefined :: [Int -> Int])) (Just [(+ 1)])
*DTContexts> baz2 (MkGADTBag (undefined :: [Int -> Int])) (Just [(+ 1)])

Итак, baz, baz2 морально эквивалентны не только в том смысле, что они возвращают один и тот же результат для одних и тех же четко определенных аргументов; но также и в том, что они демонстрируют одинаковое поведение для одних и тех же плохо определенных аргументов. Или они отличаются только тем, где сообщается об отсутствии Eq экземпляра?

@leftroundabout Прежде чем вы фактически деконструируете значение x, невозможно узнать, действительно ли применяется конструктор MkGADTBag.

Да, есть: метка поля unGADTBag определяется тогда и только тогда, когда есть совпадение с шаблоном на MkGADTBag. (Возможно, все было бы иначе, если бы для типа существовали другие конструкторы - особенно если бы они также имели метку unGADTBag.) Опять же, неопределенность / ленивая оценка не откладывают вывод типа.

Чтобы быть ясным, под [не] убедительностью я имею в виду: я могу видеть поведение и предполагаемые типы, которые я получаю. Я не вижу, чтобы лень или потенциальная неопределенность мешали выводу типов. Как я могу выявить разницу между baz, baz2, которая объяснила бы, почему у них разные типы?


person AntC    schedule 31.07.2020    source источник
comment
Для quux x = const x (x == x) GHC делает вывод quux :: Eq a => a -> a. unGADTBag требует MkGADTBag требует Eq a так же, как quux, независимо от того, оценивается ли проверка на равенство или применяется Eq словарь. Тогда почему unGADTBag теряет Eq a?   -  person AntC    schedule 05.08.2020


Ответы (4)


Вызов функций никогда не приводит к ограничению класса типа в области видимости, только (строгое) сопоставление с образцом.

Сравнение

unGADTBag x == fromJust y

по сути является вызовом функции вида

foo (unGADTBag x) (fromJust y)

где foo требует Eq a. Это было бы морально обеспечено unGADTBag x, но это выражение еще не оценено! Из-за лени unGADTBag x будет оцениваться только тогда (и если) foo потребует свой первый аргумент.

Итак, чтобы вызвать foo в этом примере, нам нужно заранее оценить его аргумент. Хотя Haskell мог бы работать таким образом, это была бы довольно неожиданная семантика, в которой аргументы оцениваются или нет, в зависимости от того, предоставляют ли они необходимое ограничение класса типа. Представьте себе более общие случаи, такие как

foo (if cond then unGADTBag x else unGADTBag z) (fromJust y)

Что здесь нужно оценивать? unGADTBag x? unGADTBag y? Оба? cond тоже? Сложно сказать.

Из-за этих проблем Haskell был спроектирован таким образом, что нам нужно вручную требовать оценку значения GADT, такого как x, с использованием сопоставления с образцом.

person chi    schedule 31.07.2020
comment
Вопрос в выводе типов - или нет? Так как же лень / время оценки могут повлиять на типизацию, которая происходит во время компиляции? Нам не нужно оценивать x или unGADTBag x, чтобы знать, что внутри (?) Есть ограничение. - person AntC; 31.07.2020
comment
@AntC Нам нужно это оценить. Рассмотрим определение x = x :: GADTBag (Int->Int), в котором используется бесконечная рекурсия. Этот тип проверяет, но внутри x нет словаря для ограничения Eq (Int -> Int) (действительно, построить такой словарь было бы даже невозможно). Мы не можем вызвать ==, если сначала не оценим x и не извлечем словарь внутри. В строгом (неленивом) языке любая переменная x с типом GADTBag (Int->Int) обязательно будет содержать словарь, но на ленивом языке она может включать в себя не завершение и не обеспечивать этого. - person chi; 31.07.2020
comment
Для вызова foo требуется Eq. Вызов foo происходит до вызова unGADTBag. Следовательно, Eq для foo не может быть результатом unGADTBag. Следовательно, это должно исходить из контекста. Таким образом, вы получите именно такой тип. - person Fyodor Soikin; 31.07.2020
comment
@AntC Об аспектах времени компиляции: это правда, что типизация - это время компиляции, но типизация должна гарантировать, что генерация кода (= компиляция в сборку) возможна с соблюдением семантики. Чтобы сделать это возможным, семантика должна требовать оценки unGADTBag x, если требуется ее словарь, с которым очень сложно работать. В вашем типе любое (уже оцененное) значение типа GADTBag a реализовано как пара: словарь Eq a и значение [a]. Нам нужно использовать первый компонент для вызова == на a, поэтому мы должны сначала оценить это значение GADTBag a. - person chi; 31.07.2020
comment
@AntC Эта семантика действительно уродлива, поэтому Haskell ее не использует, но требует, чтобы программист использовал сопоставление с образцом, чтобы сделать порядок оценки более явным, позволяя компилятору извлекать словарь в предсказуемой пользователем точке. Это требование выполняется в системе типов, которая, как вы заметили, отклоняет ваш код, поскольку в дальнейшем не будет возможности продолжить генерацию кода сборки. - person chi; 31.07.2020
comment
Спасибо. Я думаю, что для случаев использования пакетов / наборов / упорядоченных списков я надеялся, что вывод типа сможет увидеть множество мест, где есть Eq a, и вывести их все в самый широкий диапазон (из a), поэтому словарь будет оценен / востребован только один раз. DatatypeContexts по крайней мере помещает ограничение во внешнюю область типа данных; и помещает его в конструктор типа, а не в конструкторы данных; но плохо справляется с распознаванием / изучением всех необходимых мест. Я не вижу, чтобы GADT с ограничениями отвечали всем требованиям, которые DatatypeContexts пытаются (и не могут) выполнить. - person AntC; 03.08.2020

Почему тип для unGADTBag не может сообщить нам Eq a?

Прежде чем вы фактически деконструируете значение x, невозможно узнать, действительно ли применяется конструктор MkGADTBag. Конечно, если это не так, у вас есть другие проблемы (внизу), но они, вероятно, могут не всплыть. Рассмотреть возможность

ignore :: a -> b -> b
ignore _ = id

baz2' :: GADTBag a -> Maybe [a] -> Bool
baz2' x y = ignore (unGADTBag x) (y==y)

Обратите внимание, что теперь я могу вызывать функцию, скажем, с undefined :: GADTBag (Int->Int). Не должно быть проблем, ведь undefined это ignored, верно, ? Проблема в том, что, несмотря на то, что Int->Int не имел экземпляра Eq, я смог написать y==y, который y :: Maybe [Int->Int] фактически не может поддерживать.

Итак, у нас не может быть того, что только упоминания unGADTBag будет достаточно, чтобы выбросить Eq a ограничение в окружающую его область действия. Вместо этого мы должны четко ограничить область действия этого ограничения тем, где мы подтвердили, что конструктор MkGADTBag действительно применяется, и сопоставление с шаблоном выполняет это.


Если вас раздражает то, что мой аргумент основан на undefined, обратите внимание, что та же проблема возникает также при наличии нескольких конструкторов, которые вносят различные ограничения в область видимости.


Альтернатива сопоставлению с образцом, которая действительно работает, заключается в следующем:

{-# LANGUAGE RankNTypes #-}

withGADTBag :: GADTBag a -> (Eq a => [a] -> b) -> b
withGADTBag (MkGADTBag x) f = f x

baz3 :: GADTBag a -> Maybe [a] -> Bool
baz3 x y = withGADTBag x (== fromJust y)

Ответ на правки

Все это дает *** Exception: Prelude.undefined:

Да, конечно, потому что вы фактически оцениваете x == y в своей функции. Таким образом, функция может дать значение, отличное от , только если входы имеют NF. Но это далеко не все функции.

Принимая во внимание, что эти два дают ошибку одного и того же типа во время компиляции

Конечно, это так, потому что вы пытаетесь обернуть значение типа, отличного от Eq, в конструкторе MkGADTBag, который явно требует этого ограничения (и позволяет вам явно развернуть его снова!), Тогда как тип GADTBag не требует этого ограничения. (В этом вся суть такого рода инкапсуляции!)

Before you've actually deconstructed the x value, there's no way of knowing that the `MkGADTBag` constructor indeed applies.
Yes there is: field label `unGADTBag` is defined if and only if there's a pattern match on `MkGADTBag`.

Возможно, именно так должны работать метки полей, но в Haskell это не так. Метка поля - это не что иное, как функция от типа данных к типу поля, и к тому же функция nontotal, если существует несколько конструкторов.
Да, записи Haskell являются одними из худших по дизайну особенности языка. Лично я предпочитаю использовать метки полей только для больших, простых и старых типов данных с одним конструктором (и даже тогда я предпочитаю использовать не метки полей напрямую, а линзы, производные от них).

В любом случае, я не понимаю, как «метка поля определяется при наличии совпадения с шаблоном» может быть реализована таким образом, чтобы ваш код работал так, как вы думаете. Компилятору придется где-то вставить шаг подтверждения применения конструктора (и извлечения его ограничения, инкапсулированного в GADT). Но где? В вашем примере это достаточно очевидно, но в целом x может занимать обширную область с множеством ветвей решений, и вы действительно не хотите, чтобы она оценивалась в ветке, где ограничение на самом деле не требуется.

Также имейте в виду, что, когда мы спорим с _25 _ / _ 26_, речь идет не только о фактическом расхождении вычислений, чаще вы беспокоитесь о вычислениях, которые просто займут много времени (просто в Haskell на самом деле нет понятия «взять много времени").

person leftaroundabout    schedule 31.07.2020
comment
SPJ официально заявляет (повсюду), что DatatypeContexts являются неправильной функцией, потому что они требуют доказательства ограничения в тех местах, где вы ожидаете, что вывод типа предоставит это. Но это кажется отличным способом выразить то, что происходит с предполагаемым baz2 Eq a. Вся суть такого рода инкапсуляции заключается в том, что для типа Set / Bag я хочу, чтобы ограничение распространялось, а не инкапсулировалось. Таким образом, в этом случае GADT кажутся неправильной функцией. (То есть неправильное представление по-другому.) - person AntC; 02.08.2020
comment
Напротив, в случае использования GADT для более богатой типизации EDSL существует множество конструкторов, каждый с различными ограничениями / типами при спуске по синтаксическому дереву. Набор / Сумка имеет один и тот же тип содержимого / одно и то же ограничение по всему дереву - я предполагаю, что это Набор, представленный как двоичное сбалансированное дерево. - person AntC; 02.08.2020

Об этом можно подумать OutsideIn (X) ... с местными допущениями. Дело не в неопределенности или ленивых оценках. Сопоставление с образцом в конструкторе GADT находится снаружи, правая часть уравнения находится внутри. Ограничения из конструктора доступны только локально - только внутри.

baz (MkGADTBag x) (Just y) = x == y

Имеет явный конструктор данных MkGADTBag снаружи, предоставляющий Eq a. x == y вызывает запрос Eq a локально / внутри, который выводится из сопоставления с образцом. OTOH

baz2           x        y  = unGADTBag x == fromJust y

Не имеет явного конструктора данных снаружи, поэтому контекст не предоставляется. unGADTBag имеет Eq a, но он глубже внутри l.h. аргумент ==; вывод типа не заглядывает глубже. Это просто не так. Тогда в действующем определении для unGADTBag

unGADTBag (MkGADTBag x) = x

есть Eq a, доступный извне, но он не может уйти из RHS в среду типов на сайте использования для unGADTBag. Это просто не так. Грустный!

Лучшее, что я могу найти для объяснения, - это ближе к концу статьи OutsideIn, раздел 9.7 Обоснован ли акцент на основных типах? (риторический вопрос, но мой ответ хотел бы я: конечно, мы должны подчеркнуть основные типы; вывод типов может стать более принципиальным при некоторых обстоятельствах.) В последнем разделе рассматривается этот пример.

    data R a where
      RInt :: Int -> R Int
      RBool :: Bool -> R Bool
      RChar :: Char -> R Char

    flop1 (RInt x) = x

есть третий тип, который, возможно, более желателен [для flop1], и это тип R Int -> Int.

Определение flop1 имеет ту же форму, что и unGADTBag, с ограничением a равным Int.

    flop2 (RInt x) = x
    flop2 (RBool x) = x

К сожалению, обычные полиморфные типы слишком слабы, чтобы выразить это ограничение [что a должно быть только Int или Bool], и мы можем получить только Ɐa.R a -> a для flop2, что не исключает применения flop2 к значениям типа R Char.

Итак, на этом этапе работа, похоже, перестает пытаться уточнить основные типы:

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

привлекательно. Это просто не так.

Я вижу, что общее решение сложно / невозможно. Но для случаев использования ограниченных пакетов / списков / наборов спецификация:

  • Все конструкторы данных имеют одинаковые ограничения на параметры типа данных.
  • Все конструкторы выдают один и тот же тип (... -> T a, ... -> T [a], ... -> T Int и т. Д.).
  • Типы данных с одним конструктором тривиально этому удовлетворяют.

Чтобы удовлетворить первый пункт, для типа Set, использующего двоичное сбалансированное дерево, должно быть неочевидное определение конструктора Nil:

data OrdSet a  where
  SNode :: Ord a => OrdSet a -> a -> OrdSet a -> OrdSet a
  SNil  :: Ord a => OrdSet a                     -- seemingly redundant Ord constraint

Даже в этом случае повторение ограничения для каждого узла и каждого терминала кажется расточительным: это одно и то же ограничение на всем пути вниз (в отличие от GADT для абстрактных синтаксических деревьев EDSL); предположительно каждый узел содержит копию точно такого же словаря.

Лучший способ обеспечить одинаковые ограничения для каждого конструктора - это просто добавить ограничение к типу данных:

data Ord a => OrdSet a  where ...

И, возможно, ограничение могло бы пойти «OutsideOut» в среду, которая обращается к дереву.

person AntC    schedule 06.08.2020

Другой возможный подход - использовать PatternSynonym с явная подпись, дающая Обязательное ограничение.

pattern EqGADTBag :: Eq a => [a] -> GADTBag a              -- that Eq a is the *Required*
pattern EqGADTBag{ unEqGADTBag } = MkGADTBag unEqGADTBag   -- without sig infers Eq a only as *Provided*

То есть без этой явной сигнатуры:

*> :i EqGADTBag
pattern EqGADTBag :: () => Eq a => [a] -> GADTBag a

() => Eq a => ... показывает Eq a Предоставлено, возникающее из конструктора GADT.

Теперь мы получаем оба предполагаемых baz, baz2 :: Eq a => GADTBag a -> Maybe [a] -> Bool:

baz (EqGADTBag x) (Just y) = x == y

baz2           x        y  = unEqGADTBag x == fromJust y

Из любопытства: можно дать эти уравнения для baz, baz2, а также уравнения в O.P., используя имена из GADT decl. GHC предупреждает о наложении шаблонов [правильно]; и выводит ограниченный знак для baz.

Интересно, есть ли здесь шаблон дизайна? Не накладывайте ограничений на конструктор данных - то есть не делайте его GADT. Вместо этого объявите «тень» PatternSynonym с обязательными / предоставленными ограничениями.

person AntC    schedule 07.03.2021