Слияние / объединение двух классов в один в Haskell - PullRequest
4 голосов
/ 17 июля 2010

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

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 могут возникнуть позже (и я не вижу простого способа справиться с этим).
Я полагаю, что должен быть какой-то "обходной путь", чтобы получить то же поведение.

PS Варианты типа:

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

1 Ответ

3 голосов
/ 17 июля 2010

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

Теперь, причина, почему этот подходне будет "хорошо", это то, что ограничения класса в основном безотзывные .Как только 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

Обратите внимание, что, если оба предиката имеют значение true, мы явно выбираемA экземпляр.Закомментированный экземпляр делает то же самое, но вместо него использует B.Вы также можете удалить оба, и в этом случае вы получите исключительную дизъюнкцию двух классов.bval здесь я использую класс TypeBool.Обратите внимание также на сигнатуры типов, чтобы получить правильный тип boolean - для этого требуется 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
...