Насколько я знаю, нет «хороший» способ для достижения этой цели. Вы застряли в том, чтобы где-то добавить рывок. Поскольку вы не хотите типы обертки, другой вариант, о котором я могу думать, вступает во взаимодействие с определениями классов, а это значит, что мы отключены от типа метапрограммирования.
Теперь причина, по которой этот подход не будет «приятным», заключается в том, что ограничения класса в основном безотзывные. Как только 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
. Обратите внимание также на сигнатуры типа для получения правильного типа 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
код, любезно Oleg.
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
Это выглядит как реальный рабочий процесс (особенно с задержкой, разрешающей выбор другого перекрывающегося экземпляра). Нет смысла использовать это, когда вам еще нужно объявить 'IsA Int Yes'. Мне нужно приклеить несколько внешних наборов экземпляров, и я не могу использовать 'A a => IsA a Yes', потому что он выведет' A a'. – ony
@ony: То, что вы пытаетесь сделать, действительно не подходит для того, как работают классы классов Haskell. Если вы не хотите использовать обертки типов (что, вероятно, самый «идиоматический» подход) или писать дополнительные объявления экземпляров, осталось немного вариантов. Возможно, попробуйте использовать Template Haskell, чтобы просто создать экземпляр 'AB' для каждого типа, который является экземпляром' A' или 'B'? –
Падение в TH похоже на выпадение из Haskell. Не будет никакой забавы. – ony