Слияние/объединение двух классов в один в Haskell

У меня есть два непересекающихся набора типов, и я хочу создать другой набор, объединяющий эти два. Пример кода:

class A a
class B b
class AB ab

instance A a => AB a
instance B b => AB b

GHC 6.12.3 не позволяет объявить это сообщением об ошибке:

    Duplicate instance declarations:
      instance (A a) => AB a -- Defined at playground.hs:8:9-19
      instance (B b) => AB b -- Defined at playground.hs:9:9-19

Я понимаю, что это объявление приводит к потере контроля над перекрывающимися экземплярами AB a, потому что экземпляры для A a и B b могут возникнуть позже (и я не вижу простого способа справиться с этим). вокруг», чтобы получить такое же поведение.

P.S. Такие варианты:

newtype A a => WrapA a = WrapA a
newtype B b => WrapB b = WrapB b

instance A a => AB (WrapA a)
instance B b => AB (WrapB b)

и

data WrapAB a b = A a => WrapA a
                | B b => WrapB b

instance AB (WrapAB a b)

и любой другой, который обертывает некоторые из этих типов, не соответствует моим потребностям (выбор реализации по объявленному третьей стороной классу типа)

Комментарий к @camccann: Отличная идея добавить флаг для управления слиянием/выбором типа на флаге, но я хотел бы избежать таких вещей, как гонки перекрывающихся экземпляров. Для тех, кто заинтересован в этом ответе, сжатый вариант:

data Yes
data No

class IsA a flag | a -> flag
class IsB b flag | b -> flag

instance Delay No flag => IsA a flag
instance Delay No flag  => IsB b flag

instance (IsA ab isA, IsB ab isB, AB' isA isB ab) => AB ab

class AB' isA isB ab
instance (A a) => AB' Yes No a
instance (B b) => AB' No Yes b
instance (A a) => AB' Yes Yes a

class Delay a b | a -> b
instance Delay a a

instance IsA Bool Yes
instance A Bool

person ony    schedule 17.07.2010    source источник


Ответы (1)


Насколько я знаю, нет "хорошего" способа сделать это. Вы где-то застряли с добавлением хлама. Поскольку вам не нужны типы-оболочки, другой вариант, который я могу придумать, — это вместо этого возиться с определениями классов, что означает, что мы уходим в страну метапрограммирования типов.

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

Чтобы обойти это, нам нужны предикаты типов с логическими значениями уровня типа, а не прямые ограничения класса. Для этого мы используем многопараметрические классы типов с функциональными зависимостями для создания функций типов и перекрывающиеся экземпляры с отложенной унификацией для написания "экземпляров по умолчанию".

Во-первых, нам нужны забавные языковые прагмы:

{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE UndecidableInstances #-}

Определите некоторые логические значения уровня типа:

data Yes = Yes deriving Show
data No = No deriving Show

class TypeBool b where bval :: b
instance TypeBool Yes where bval = Yes
instance TypeBool No where bval = No

Класс TypeBool не является строго необходимым — я в основном использую его, чтобы не работать с undefined.

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

class (TypeBool flag) => IsA a flag | a -> flag
class (TypeBool flag) => IsB b flag | b -> flag 

instance (TypeBool flag, TypeCast flag No) => IsA a flag
instance (TypeBool flag, TypeCast flag No) => IsB b flag

Ограничение TypeCast — это, конечно же, печально известный класс унификации типов Олега. Код для этого можно найти в конце этого ответа. Здесь необходимо отложить выбор типа результата — fundep говорит, что первый параметр определяет второй, а экземпляры по умолчанию являются полностью универсальными, поэтому размещение No непосредственно в заголовке экземпляра будет интерпретироваться как предикат, всегда оцениваемый как false, что не полезно. Использование TypeCast вместо этого ожидает, пока GHC не выберет наиболее конкретный перекрывающийся экземпляр, что заставляет результат быть No тогда и только тогда, когда более конкретный экземпляр не может быть найден.

Я собираюсь внести еще одну не совсем необходимую корректировку в сами классы типов:

class (IsA a Yes) => A a where
    fA :: a -> Bool
    gA :: a -> Int

class (IsB b Yes) => B b where
    fB :: b -> Bool
    gB :: b -> b -> String

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

Затем класс объединения разделяется на две части. Первый имеет единственный универсальный экземпляр, который просто применяет предикаты членства и вызывает второй, который сопоставляет результаты предиката с фактическими экземплярами.

class AB ab where 
    fAB :: ab -> Bool
instance (IsA ab isA, IsB ab isB, AB' isA isB ab) => AB ab where
    fAB = fAB' (bval :: isA) (bval :: isB)

class AB' isA isB ab where fAB' :: isA -> isB -> ab -> Bool
instance (A a) => AB' Yes No a where fAB' Yes No = fA
instance (B b) => AB' No Yes b where fAB' No Yes = fB
instance (A ab) => AB' Yes Yes ab where fAB' Yes Yes = fA
-- instance (B ab) => AB' Yes Yes ab where fAB' Yes Yes = fB

Обратите внимание: если оба предиката истинны, мы явно выбираем экземпляр A. Закомментированный экземпляр делает то же самое, но вместо этого использует B. Вы также можете удалить оба, и в этом случае вы получите исключительную дизъюнктуру двух классов. Здесь bval я использую класс TypeBool. Обратите также внимание на сигнатуры типов, чтобы получить правильное логическое значение типа — для этого требуется ScopedTypeVariables, который мы включили выше.

В завершение, некоторые экземпляры, которые стоит попробовать:

instance IsA Int Yes
instance A Int where
    fA = (> 0)
    gA = (+ 1)

instance IsB String Yes
instance B String where
    fB = not . null
    gB = (++)

instance IsA Bool Yes
instance A Bool where
    fA = id
    gA = fromEnum

instance IsB Bool Yes
instance B Bool where
    fB = not
    gB x y = show (x && y)

Пробуем в GHCI:

> fAB True
True
> fAB ""
False
> fAB (5 :: Int)
True
> fAB ()
No instance for (AB' No No ())
  . . .

А вот код TypeCast, любезно предоставленный Олегом.

class TypeCast   a b   | a -> b, b->a   where typeCast   :: a -> b
class TypeCast'  t a b | t a -> b, t b -> a where typeCast'  :: t->a->b
class TypeCast'' t a b | t a -> b, t b -> a where typeCast'' :: t->a->b
instance TypeCast'  () a b => TypeCast a b where typeCast x = typeCast' () x
instance TypeCast'' t a b => TypeCast' t a b where typeCast' = typeCast''
instance TypeCast'' () a a where typeCast'' _ x  = x
person C. A. McCann    schedule 17.07.2010
comment
Это выглядит как реальный обходной путь (особенно с задержкой разрешения типа для выбора другого перекрывающегося экземпляра). Нет смысла использовать это, когда вам все еще нужно объявлять IsA Int Yes. Мне нужно склеить несколько внешних наборов экземпляров, и я не могу использовать A a => IsA a Yes, потому что это выведет A a. - person ony; 18.07.2010
comment
@ony: Ну, то, что вы пытаетесь сделать, действительно не очень хорошо согласуется с тем, как работают классы типов Haskell. Если вы не хотите использовать обертки типов (что, вероятно, является наиболее идиоматичным подходом) или писать дополнительные объявления экземпляров, остается не так много вариантов. Возможно, попробуйте использовать Template Haskell, чтобы просто сгенерировать экземпляр AB для каждого типа, который является экземпляром A или B? - person C. A. McCann; 18.07.2010
comment
падение в TH похоже на выпадение из Haskell. Не будет веселья. - person ony; 18.07.2010
comment
@ony: Я не буду спорить иначе. Сцилла или Харибда, выбирай. - person C. A. McCann; 18.07.2010