Получение `Show a` из контекста` Show (a, b) `

Как сказано в названии, я заинтересован в использовании Show a в контексте, в котором у меня есть Show (a,b). Эта проблема легко возникает с GADT следующим образом:

data PairOrNot a where
  Pair :: (b,c) -> PairOrNot (b,c)
  Not :: a -> PairOrNot a

showFirstIfPair :: Show a => PairOrNot a -> String
showFirstIfPair (Not a) = show a
showFirstIfPair (Pair (b,c)) = show b

Ошибка:

Could not deduce (Show b) arising from a use of ‘show’
from the context (Show a)
  bound by the type signature for
             showFirstIfPair :: Show a => PairOrNot a -> String
  at app/Main.hs:24:20-50
or from (a ~ (b, c))
  bound by a pattern with constructor
             Pair :: forall b c. (b, c) -> PairOrNot (b, c),
           in an equation for ‘showFirstIfPair’
  at app/Main.hs:26:18-27
Possible fix:
  add (Show b) to the context of the data constructor ‘Pair’
In the expression: show b
In an equation for ‘showFirstIfPair’:
    showFirstIfPair (Pair (b, c)) = show b

Я бы подумал, что объявление экземпляра instance (Show a, Show b) => Show (a,b) доказывает Show element, но я также могу представить, что проблема также как-то связана с тем, как механизм классов типов реализуется во время выполнения.

Я обнаружил, что если мы можем изменить определение класса, это можно решить с помощью:

class Show' a where
  show' :: a -> String
  unpair :: a -> Dict (a ~ (b,c)) -> Dict (Show' b, Show' c)

-- An example non-pair instance
instance Show' Int where
  show' i = ""
  unpair = undefined -- This is OK, since no one can construct Dict (Int ~ (b,c))

instance (Show' a, Show' b) => Show' (a,b) where
  show' (a,b) = ""
  unpair _ Dict = Dict -- In this context we have access to Show' for elems

Затем на сайте использования мы явно получаем словарь:

showFirstIfPair :: Show' a => PairOrNot a -> String
showFirstIfPair (Not a) = show' a
showFirstIfPair (Pair a@(b,c)) = 
  case unpair a Dict of -- This is a Dict (a~(b,c))
    Dict -> show' b -- This is Dict (Show' b,Show' c)

Мне было интересно, есть ли ненавязчивый (или просто другой) способ получения Show element. Если нет, не могли бы вы объяснить, почему именно возникает эта проблема?


person enobayram    schedule 22.09.2016    source источник
comment
Что ж, вы должны знать (Show b, Show c), чтобы построить словарь Show (b, c). Итак, в точке, где вы создаете Pair, вы можете вместо этого просто извлечь первый элемент   -  person Benjamin Hodgson♦    schedule 22.09.2016
comment
Ваш вопрос подразумевает, что у вас есть контекст Show a, Show b => ..., но в вашем коде есть только Show a => ..., поэтому возникает ошибка о невозможности вывести Show b.   -  person Lee    schedule 22.09.2016
comment
Вы путаете значение термина "следование классу типов". Show a, Show b => Show (a,b) означает, что Show a и Show b вместе дают Show (a,b), но это не обязательно означает противоположное. Например, рассмотрим два типа X, Y с instance Show (X, Y) - у вас не обязательно есть здесь instance (Show X), потому что show может быть const "". Ваша unpair функция дает подтверждение того свойства, которое вы хотите, и это именно то, что вы должны сделать, если хотите использовать указанное свойство.   -  person user2407038    schedule 22.09.2016
comment
@ user2407038 Может быть, я смотрю на это с неправильной точки зрения, если Z истинно тогда и только тогда, когда X и Y, не Z подразумевает и X, и Y? Может, мне не следует думать об этом как о логическом утверждении.   -  person enobayram    schedule 22.09.2016
comment
@ user2407038 Ясно, значит, если бы OverlappingInstances был включен, кто-то мог бы определить мошенника instance Show (X, Y) без Show X или Show Y.   -  person enobayram    schedule 22.09.2016
comment
@enobayram Логическое утверждение A /\ B => C означает, что и A, и B вместе являются достаточным условием для C, что ничего не говорит о том, что происходит в отношении A и B, когда C определенно выполняется. Он не утверждает, что C выполняется тогда и только тогда, когда A /\ B. То, что вы описываете, обычно пишется A /\ B <=> C или A /\ B == C.   -  person user2407038    schedule 22.09.2016
comment
@Lee Вы ошибаетесь, у меня есть контекст (Show a, a ~ (b,c)) и мне нужно Show b   -  person enobayram    schedule 22.09.2016
comment
@BenjaminHodgson Конечно, но это означает, что мне нужно изменить определение данных   -  person enobayram    schedule 22.09.2016
comment
@enobayram Да, я считаю, что вам понадобится OverlappingInstances где-то для конкретного примера, который я привел, но дело в том, что компилятор не может предположить, что Show (a,b) пришел из (Show a, Show b), независимо от того, что вы делаете, потому что это предположение просто не звук. Это всего лишь предположение об открытом мире для классов типов в несколько хитрой форме.   -  person user2407038    schedule 22.09.2016
comment
@ user2407038 Что ж, если бы компилятор мог предполагать и открытый мир, и отсутствие перекрытий, он мог бы вывести Show a из Show (a,b). Но Haskell хочет допустить возможность перекрытия, поэтому он должен запретить этот необоснованный вывод.   -  person chi    schedule 22.09.2016
comment
@chi, GHC Haskell хочет разрешить возможность открытых перекрытий. Некоторые хаскелисты, вроде меня, думают, что это ужасная идея. Однако есть веские причины для реализации воздерживаться от предложения следования на основе ограничений экземпляра. Словарь ограничений (для ограничения класса) может быть встроен в словарь экземпляра и не передаваться во время выполнения. GHC Core, кажется, представляет собой экземпляр с контекстом как функцию от словарей до словаря. Учитывая f x y, вы не можете ожидать восстановления x или y.   -  person dfeuer    schedule 23.09.2016


Ответы (3)


Если вы не возражаете против ограничения, согласно которому b всегда должен быть экземпляром Show, то это простое решение:

data PairOrNot a where
  Pair :: Show b => (b,c) -> PairOrNot (b,c)
  Not :: a -> PairOrNot a
person user2297560    schedule 22.09.2016
comment
Я не знаю, почему ваш ответ был отклонен, ведь я указал, что открыт для различных решений, и это решение не более навязчиво, чем мое собственное. Я вторгаюсь в класс типов, вы вторгаетесь в определение данных. - person enobayram; 22.09.2016
comment
Спасибо. Вероятно, голос против был вызван тем, что добавление ограничений в конструкторы, как правило, не одобрялось. - person user2297560; 22.09.2016
comment
@ user2297560 Нет, это не осуждается, AFAIK. Не одобряются ограничения конструкторов типов данных, таких как data Show a => T a = ..., поскольку они имеют сомнительную и неудобную семантику. (Верно, однако, что, выполняя Pair :: Show b => ..., мы заставляем конструктор оперативно упаковать еще один указатель, увеличивая объем памяти, занимаемый представлением значения) - person chi; 22.09.2016
comment
@ user2297560 Нет, в этом весь смысл GADT: иметь хороший способ добавления ограничений к конструкторам значений. - person Bakuriu; 23.09.2016

Я все время пытался понять, смогу ли я придумать что-нибудь получше, и вот лучшее, что я мог придумать:

В своей первоначальной попытке я объединил класс типа Show' с объявлением экземпляра для пар. Несмотря на то, что я не мог найти способ изменить класс типа, мне, по крайней мере, удалось обобщить это для любого объявления экземпляра.

Как указано в комментариях к вопросу, instance (Show a, Show b) => Show (a,b) является односторонним следствием, но я предположил, что его можно применить и в другом направлении, учитывая отсутствие перекрывающихся экземпляров. К сожалению, GHC не полагается на это, но мы можем утверждать это сами. Моя интуиция может быть переведена в код: (:=> от Data.Constraint из пакета constraints, а a :=> b означает, что где-то есть instance a => b)

class Show' a where
  show' :: a -> String
  noOverlap :: (b :=> Show' a) => Proxy a -> Dict b

Здесь функция noOverlap - это обещание, что если вы сможете найти ограничение b, которое порождает экземпляр Show' a, я могу доказать b данный Show' a. Это обещание эквивалентно заявлению о том, что для Show' не будет перекрывающихся экземпляров.

Теперь нам нужна вспомогательная функция для реализации noOverlap

basedOn :: forall k a. (k :=> Show' a, k) => 
             (forall b. (b :=> Show' a) => Proxy a -> Dict b)
basedOn _ = unsafeCoerce (Dict :: Dict k)

Эта функция делает то, что, если вы вызываете ее в контексте, где у вас есть экземпляр k :=> Show' a, она вернет функцию, которая вернет Dict b для любого экземпляра b :=> Show' a. Мы должны использовать unsafeCoerce, чтобы убедить GHC, что Dict k и Dict b одинаковы, но, насколько я могу видеть, это безопасное использование unsafeCoerce, поскольку функциональная зависимость a :=> b | b -> a гарантирует, что может быть только один экземпляр k :=> Show' a для учитывая Show' a.

Теперь, учитывая этого помощника, вот как вы определяете экземпляры

-- An example non-pair instance
instance () :=> Show' Int where ins = Sub Dict 
instance        Show' Int where
  show' i = ""
  noOverlap = basedOn

instance (Show' a, Show' b) :=> Show' (a,b) where ins = Sub Dict
instance (Show' a, Show' b)  => Show' (a,b) where
  show' (a,b) = ""
  noOverlap = basedOn -- GHC does all the plumbing here

Мы должны определить :=> экземпляры вручную, поскольку GHC не делает этого автоматически, но здесь нет места для ошибки. Если мы дадим слишком слабое ограничение слева от заданного вручную :=>, ins = Sub Dict не будет компилироваться, а если мы дадим слишком сильное ограничение, то noOverlap = basedOn не сможет скомпилировать, так что компилятор принудительно заставит шаблон.

Затем мы можем использовать noOverlap следующим образом:

showFirstIfPair :: Show' a => PairOrNot a -> String
showFirstIfPair (Not a) = show' a
showFirstIfPair (Pair a@(b,c)) = 
  -- In this context we have (Show' b, Show' c) :=> Show' a
  case noOverlap Proxy of -- This is a Proxy a
    Dict -> show' b -- This is a Dict (Show' b, Show' c)

Приятно то, что теперь мы также можем перейти, скажем, от Show' [a] к Show' a или любому другому объявлению экземпляра.

Примечание. Для их компиляции вам понадобится {-# LANGUAGE FlexibleContexts, TypeOperators, RankNTypes, ConstraintKinds, AllowAmbiguousTypes #-}.

person enobayram    schedule 23.09.2016
comment
@dfeuer Да, работает. noOverlap _ = Dict не будет работать, потому что fundep в :=> недостаточно, чтобы убедить GHC в равенстве, но мы сами утверждаем равенство с unsafeCoerce в basedOn. Я полагаю, было бы довольно сложно нарушить это unsafeCoerce, даже если бы вы попытались. - person enobayram; 24.09.2016
comment
Ага! Я неправильно понял, что вы делали. Я думал, ты к чему-то еще принуждал. Извините! Для ясности вы можете рассмотреть возможность использования этого типа: basedOn :: forall k a b. (k :=> Show' a, b :=> Show' a) => proxy a -> Dict k -> Dict b. Поскольку это приводит к принудительному использованию словаря на основе предположения, что fundep действительно выполняется (не совсем безопасное предположение), вы могли бы даже пойти дальше и использовать assertThin :: forall k l c. (k :=> c, l :=> c) => proxy c -> k :~: l без (я считаю) дальнейшей потери безопасности. - person dfeuer; 28.09.2016

Вот обобщение user2297560, которое не требует жесткого кодирования Show в GADT:

{-# LANGUAGE ConstraintKinds, KindSignatures #-}
import GHC.Exts (Constraint)

data PairOrNot (cl :: * -> Constraint) (a :: *) where
  Pair :: (cl b, cl c) => (b,c) -> PairOrNot (b,c)
  Not :: cl a => a -> PairOrNot a

Тогда ты можешь

showFirstIfPair :: PairOrNot Show a -> String
showFirstIfPair (Not a) = show a
showFirstIfPair (Pair (b,c)) = show b
person leftaroundabout    schedule 23.09.2016
comment
Так лучше, но он по-прежнему связывает конкретный тип PairOrNot cl a с набором cl ограничений, переданных ему. В моем случае мне нужно, чтобы PairOrNot a был параметрическим для a без каких-либо ограничений. Мне просто нужно (Show b, Show c) из Show (b,c) в тех контекстах, где я знаю Show a. - person enobayram; 23.09.2016
comment
@enobayram, если вас интересует только Show, вы можете показать пару, а затем попытаться извлечь соответствующую часть строки. Это, конечно, ужасно. - person dfeuer; 23.09.2016