Как определить Eq-экземпляр списка без GADT или контекстов типа данных

Я использую Glasgow Haskell Compiler, Version 7.8.3, stage 2 booted by GHC version 7.6.3.

Я попытался использовать следующее определение данных для типа List в Haskell:

data Eq a => List a = Nil | Cons a (List a)

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

data List a where
 Nil :: List a
 Cons :: Eq a => a -> List a -> List a

Он работает нормально. Видимая проблема с этим решением заключается в том, что теперь мне нужно использовать флаг -XGADTs, от которого я все еще не хочу зависеть в этом случае, поскольку нет необходимости в функционировании встроенной версии списка. Есть ли способ ограничить тип в Cons значением Eq a, чтобы я мог сравнивать два списка без флагов компилятора и без использования ключевого слова derived? Оставшийся код выглядит следующим образом:

instance Eq (List a) where
 (Cons a b) == (Cons c d) = (a == c) && (b == d)
 Nil == Nil = True
 _ == _ = False
testfunction = Nil :: List Int
main = print (if testfunction == Nil then "printed" else "not printed")

Я вижу, что работает следующее решение:

data List a = Nil | Cons a (List a)
instance Eq a => Eq (List a) where
 (Cons a b) == (Cons c d) = (a == c) && (b == d)
 Nil == Nil = True
 _ == _ = False
testfunction = Nil :: List Int
main = print (if testfunction == Nil then "printed" else "not printed")

Однако по какой-то причине это не работает с ручным определением Eq (здесь Equals).

class Equal a where  
 (=+=) :: a -> a -> Bool  
 (/+=) :: a -> a -> Bool  
 x =+= y = not (x /+= y)  
 x /+= y = not (x =+= y)
data List a = Nil | Cons a (List a)
instance Equal a => Equal (List a) where
 (Cons a b) =+= (Cons c d) = (a =+= c) && (b =+= d)
 Nil =+= Nil = True
 _ =+= _ = False
testfunction = Nil :: List Int
main = print (if testfunction =+= Nil then "printed" else "not printed")

Я получаю следующую ошибку:

No instance for (Equal Int) arising from a use of ‘=+=’
    In the expression: testfunction =+= Nil
    In the first argument of ‘print’, namely
      ‘(if testfunction =+= Nil then "printed" else "not printed")’
    In the expression:
      print (if testfunction =+= Nil then "printed" else "not printed")

Однако, используя GADT, я могу показать, что мой класс Equal действительно работает. Этот код работает:

class Equal a where  
 (=+=) :: a -> a -> Bool  
 (/+=) :: a -> a -> Bool  
 x =+= y = not (x /+= y)  
 x /+= y = not (x =+= y)
data List a where
 Nil :: List a
 Cons :: Equal a => a -> List a -> List a
instance Equal (List a) where
 (Cons a b) =+= (Cons c d) = (a =+= c) && (b =+= d)
 Nil =+= Nil = True
 _ =+= _ = False
testfunction = Nil :: List Int
main = print (if testfunction =+= Nil then "printed" else "not printed")

Однако я должен использовать instance Equal (List a) where вместо instance Equal a => Equal (List a) where, иначе я получаю сообщение об ошибке:

No instance for (Equal Int) arising from a use of ‘=+=’
    In the expression: testfunction =+= Nil
    In the first argument of ‘print’, namely
      ‘(if testfunction =+= Nil then "printed" else "not printed")’
    In the expression:
      print (if testfunction =+= Nil then "printed" else "not printed")

person Timothy Swan    schedule 29.05.2015    source источник
comment
Ошибка отсутствия экземпляра для Equal Int, которую вы получаете, связана с тем, что нет экземпляра для Equal Int :-P. Вы должны определить его: instance Equal Int where ... (На самом деле довольно редко правильное решение отсутствия ошибок экземпляра состоит в том, чтобы определить экземпляр, но в этом случае это так)   -  person luqui    schedule 29.05.2015
comment
Также я заметил, что ваши исследования GADT могут мешать хорошему пониманию классов типов, фундаментальной особенности языка. Я предлагаю сделать шаг назад от GADT, пока вы не овладеете классами типов, и, возможно, изучить prelude и как определяются его классы и экземпляры.   -  person luqui    schedule 29.05.2015
comment
Упс, это ужасная прелюдия. Попробуйте из отчета   -  person luqui    schedule 29.05.2015
comment
Попробуйте использовать Cons 1 Nil :: List Int вместо Nil :: List Int; вы по-прежнему получите сообщение об ошибке No instance for (Equal Int). Проблема в том, что вы не определили, как вы можете использовать Int с =+=, прежде чем пытаться использовать List Int с =+=; это не имеет ничего общего с тем, используете ли вы подход GADT или подход ограничения экземпляра. Подход GADT не снимает бремени доказательства того, что элементы в списке можно сравнить на предмет равенства, он просто переносит бремя доказывания с сайтов использования == на строительные площадки ваших списков.   -  person Ben    schedule 29.05.2015


Ответы (2)


Похоже, вы пытаетесь ограничить списки, чтобы они могли содержать только значения, реализующие Eq, и вы не можете сделать это без расширений. Однако вы можете указать компилятору, как сравнивать два List a, когда a имеет тип, реализующий Eq. Есть два простых способа сделать это. Самый простой - с производным оператором:

data List a = Nil | Cons a (List a) deriving (Eq)

Или вы можете определить его вручную:

instance Eq a => Eq (List a) where
    (Cons a b) == (Const c d) = (a == c) && (b == d)
    Nil == Nil = True
    _ == _ = False

Теперь всякий раз, когда вы заполняете свой тип List чем-то, что реализует Eq, список также будет сопоставим с использованием ==. Нет необходимости ограничивать значения, которые могут находиться внутри Cons. У вас, конечно, может быть нормальный список функций, например

fs1 :: [Int -> Int]
fs1 = [(+1), (*3), (+2), (*4)]

Или в вашем случае

fs2 :: List (Int -> Int)
fs2 = Cons (+1) $ Cons (*3) $ Cons (+2) $ Cons (*4) Nil

Который можно использовать как

> map ($ 10) fs1
[11, 30, 12, 40]

И дано

map' :: (a -> b) -> List a -> List b
map' f Nil = Nil
map' f (Cons x xs) = Cons (f x) (map' f xs)

затем

> map' ($ 10) fs2
Cons 11 (Cons 30 (Cons 12 (Cons 40 Nil)))

Хотя для того, чтобы просмотреть его в GHCi, вы также должны получить Show:

data List a = Nil | Cons a (List a) deriving (Eq, Show)

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


Чтобы заставить его работать с вашим пользовательским классом типов Equal, вам придется написать несколько экземпляров вручную:

class Equal a where
    (=+=) :: a -> a -> Bool
    (/+=) :: a -> a -> Bool
    x =+= y = not (x /+= y)
    x /+= y = not (x =+= y)

instance Equal Int where
    x =+= y = x == y

instance Equal a => Equal (List a) where
    (Cons a b) =+= (Cons c d) = (a =+= c) && (b =+= d)
    Nil =+= Nil = True
    _ =+= _ = False

Теперь, поскольку у вас есть экземпляры Equal Int и Equal a => Equal (List a), вы можете сравнить два List Int:

> let x = Cons 1 (Cons 2 (Cons 3 Nil)) :: List Int
> let y = Cons 1 (Cons 2 (Cons 3 Nil)) :: List Int
> x =+= y
True
> x =+= Nil
False

Для любого типа, который вы хотите сохранить в List и использовать =+=, вам придется реализовать Equal для этого типа.

person bheklilr    schedule 29.05.2015
comment
В настоящее время ваш ответ работает только для класса Eq, встроенного в Haskell. Однако мне нужно решение, которое работает для любого класса. Я обновляю вопрос соответственно. - person Timothy Swan; 29.05.2015
comment
@TimothySwan Нет. Производная часть работает только с несколькими встроенными классами, но ручное определение экземпляров всегда работает. - person Cubic; 29.05.2015
comment
@ Кубик Да. Я знаю. Я только что обновил свой вопрос, чтобы показать, как ручное определение экземпляра с моим собственным определением Eq по-прежнему не работает в случае без флагов компилятора. - person Timothy Swan; 29.05.2015
comment
@bheklilr Я понимаю, почему пользовательский класс Equal не работал. Мне нужно было определить равенство для работы с типом Int, поскольку я использовал его в реальных выражениях. - person Timothy Swan; 29.05.2015

Обычным решением для этого является использование этой структуры:

data List a = Nil | Cons a (List a)

instance Eq a => Eq (List a) where
 (Cons a b) == (Cons c d) = (a == c) && (b == d)
 Nil == Nil = True
 _ == _ = False

Заметил, что я добавил Eq a в качестве ограничения к экземпляру, а не к типу данных. Таким образом, все списки, которые вы могли бы сравнить на равенство так, как вы пытаетесь его написать, можно сравнить на равенство, но вы также допускаете существование списков вещей, которые нельзя сравнивать на равенство. И это поддерживается каждой версией Haskell, с которой вы столкнетесь, даже очень старой, без расширений.

Затем этот подход также хорошо расширяется, когда вы хотите добавить экземпляр Show, экземпляр Ord и т. д.; чтобы сделать это по-своему, вы должны продолжать возвращаться и делать структуру данных более ограничительной, добавляя больше ограничений (потенциально ломая существующий код, который работал нормально, потому что ему не нужны были эти экземпляры). В то время как если вы оставите тип данных без ограничений и просто сделаете свои экземпляры Show a => Show (List a), Ord a => Ord (List a) и т. д., тогда вы сможете показывать и списки порядков типов, которые поддерживают оба типа, но вы все равно можете иметь (и показывать) списки типов, которые поддерживают Show, но не Ord, и наоборот.

Кроме того, существует множество полезных классов типов для параметризованных типов (таких как List a), которые требуют, чтобы они были полностью универсальными в своем параметре типа. Примеры: Functor, Applicative, Monad; невозможно правильно реализовать их для типа списка с ограничениями, который вы пытаетесь создать.

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

person Ben    schedule 29.05.2015
comment
Похоже, что это решение работает, пока я действительно использую встроенный класс Eq. Как ни странно, специально созданный класс Eq, который в остальном работает, не работает с этим решением. Я определил его следующим образом: class Equal a where (=+=) :: a -> a -> Bool (/+=) :: a -> a -> Bool x =+= y = not (x /+= у) х /+= у = нет (х =+= у) - person Timothy Swan; 29.05.2015
comment
@TimothySwan, во встроенном классе Eq нет ничего особенного, за исключением того, что у него есть куча определенных экземпляров для встроенных типов. Вы должны быть в состоянии эмулировать его полностью, в том числе с помощью этого решения (которое является стандартным решением вашей проблемы, и я делал это много раз) - person luqui; 29.05.2015