Класс типа из подмножества класса рекурсивного типа (или тип из рекурсивного типа)

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

Вот пример:

data Atom = Atom
data (Formula a) => Negation a = Negation a

class Formula a where
instance Formula Atom where
instance (Formula a) => Formula (Negation a) where

class SubFormula a where
instance SubFormula Atom where

Этот код прекрасно компилируется, но когда я добавляю функцию, которая преобразует экземпляр класса супертипа в один из классов подтипа

formulaToSubFormula :: (Formula a, SubFormula b) => a -> b
formulaToSubFormula _ = Atom

я получаю сообщение об ошибке

test.hs:12:25:
    Could not deduce (b ~ Atom)
    from the context (Formula a, SubFormula b)
      bound by the type signature for
                 formulaToSubFormula :: (Formula a, SubFormula b) => a -> b
      at test.hs:12:1-28
      `b' is a rigid type variable bound by
          the type signature for
            formulaToSubFormula :: (Formula a, SubFormula b) => a -> b
          at test.hs:12:1
    In the expression: Atom
    In an equation for `formulaToSubFormula':
        formulaToSubFormula _ = Atom

Мое первоначальное намерение состояло в том, чтобы сделать это с обычными типами, но с классами типов проблема кажется более доступной и в целом более гибкой.

Например:

data Formula = Atom | Negation Formula | Conjunction Formula Formula
data SubFormula = Atom | Negation SubFormula

Редактировать

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

Расширенный пример (логика высказываний; недопустимый синтаксис Haskell):

data Formula = Atom
             | Negation Formula
             | Disjunction Formula Formula
             | Implication Formula Formula
data SimpleFormula = Atom
                   | Negation SimpleFormula
                   | Disjunction SimpleFormula SimpleFormula

-- removeImplication is not implemented correctly but shows what I mean
removeImplication :: Formula -> SimpleFormula
removeImplication (Implication a b) = (Negation a) `Disjunction` b
removeImplication a = a

Позже у меня может быть формула в конъюнктивной нормальной форме (нет допустимого синтаксиса Haskell)

data CNF = CNFElem
         | Conjunction CNF CNF
data CNFElem = Atom
             | Negation Atom
             | Disjunction CNFElem CNFElem

Поэтому мне нужен инструмент для представления этой иерархии.


person Till Theis    schedule 27.04.2011    source источник
comment
Обратите внимание, что в плане removeImplication вы удаляете только самый внешний конструктор Implication. removeImplication (Negation (Implication x y)) будет (Negation (Implication x y)). Простое решение состоит в том, чтобы просто иметь два типа Formula и, скажем, NormalForm и функцию toNormalForm, которая проходит через сложные формулы и рекурсивно преобразует Импликации в соответствующую NormalForm, не так ли? Я думаю, нам все еще нужно знать больше о том, куда вы идете с этим.   -  person applicative    schedule 28.04.2011
comment
@applicative: функция removeImplication нужна только для того, чтобы показать, что я имею в виду. Дело в том, что NormalForm является подмножеством Formula, и поэтому я ищу способ разделить это подмножество между обоими типами.   -  person Till Theis    schedule 28.04.2011
comment
До сих пор неясно, какова ваша более широкая цель; например, стоит ли идти по одному из маршрутов, упомянутых stephen tetley. Определение типа данных, особенно рекурсивное, похоже на язык в миниатюре. Вы рассматриваете несколько маленьких языков и их взаимоотношения; почему эти отношения не должны быть выражены функциями между типами? Если L1 и L2 являются типами, то типы L1 -> L2 и L2 -> L1 являются типами отношений между ними, грубо говоря.   -  person applicative    schedule 29.04.2011
comment
Я так понимаю, вы знаете, что в data CNFElem и data CNF вам нужно обернуть первый «конструктор». В его нынешнем виде каждый из них является уже определенным типом, Atom и CNFElem соответственно, и не может быть конструктором типа, который вы определяете. Итак, вам нужны data CNFElem = CNFAtom Atom | ... и data CNF = CNFElt CNFElem. Это относится и к большинству определений типов ранее в запросе.   -  person applicative    schedule 29.04.2011
comment
@applicative: я никогда не думал о разных типах формул / данных как о разных языках, потому что каждый CNF-Formula является допустимым Formula (хотя не каждый Formula является допустимым CNF-Formula). Поэтому я попытался визуализировать это «наследство» через систему типов.   -  person Till Theis    schedule 29.04.2011
comment
Предложение Стивена Тетли интересно, и я рассмотрю возможность использования его подхода. Что касается неправильного определения данных: вы правы, я просто хотел показать структуру, но смешал ее с синтаксисом Haskell.   -  person Till Theis    schedule 29.04.2011
comment
Каждая CNF-формула является действительной формулой (хотя не каждая формула является действительной CNF-формулой). -- Ну, это означает, что вы можете использовать isFormula и isCNFFormula как предикат Strings, или же использовать isCNFFormula как предикат Formulas. Он латентно отвергает представление обоих как типов, то есть как нечто, предполагаемое представлением его обитателей. Все B являются C предполагает, что существует общий тип, скажем, A, и мы можем утверждать или отрицать, что B является B и является C для A, или что B является базовым типом, а предикат C определяется по отношению к Это.   -  person applicative    schedule 30.04.2011
comment
Я хотел добавить к упомянутым stephen tetley статье [Типы данных на выбор] (www.cs.nott.ac.uk/~wss/Publications/DataTypesALaCarte.pdf). Вот модуль, содержащий некоторые материалы hpaste.org/46175/data_types_a_la_carte. Я не могу вспомнить, где я нашел это.   -  person applicative    schedule 30.04.2011
comment
Соответствующий материал предшествует строке 124. Его Val соответствует вашему типу Atom, его Add и Mul соответствуют вашим Disjunction и Implication (здесь нам нужно отрицание). (:+:) добавляет один ингредиент к другому. Итак, сначала у нас есть data Val e = Val Int (обратите внимание на неиспользуемый параметр); затем data Add e = Add e e, своего рода диагонализатор, тоже функтор. Вместе они дают нам Val :+: Add, еще один функтор. Затем мы добавляем data Mul e = Mul e e и можем рассматривать (Val :+: Add) :+: Mul, новый экземпляр Functor.   -  person applicative    schedule 30.04.2011
comment
Expr принимает фиксированную точку любого из них, таким образом, Expr (Val :+: Add), один тип, имеет суммы и суммы сумм и т. д. (Листья Ints из-за определения Val) Expr ((Val :+: Add) :+: Mul) представляет суммы, произведения, суммы произведений и т. д. Наконец, класс многопараметрического типа (:<:) имеет вид отношения «подтип», но обратите внимание, что он связывает функторы в системе типов. Я не изучал его какое-то время; это сложно, но очень интересно и сравнительно легко читается.   -  person applicative    schedule 30.04.2011
comment
@applicative: Упомянутая вами статья действительно интересна. Но я, честно говоря, понятия не имею, как реализовать такую ​​структуру, как CNF (мой последний пример). Мне пришлось бы написать type CNF = Expr (Conjunction :+: Disjunction :+: Atom :+: Negation), но это позволило бы произвольное вложение Conjunction и Disjunctions, хотя Disjunction не может содержать Conjunction (то же самое для Negation).   -  person Till Theis    schedule 02.05.2011
comment
Если бы мне все равно пришлось прибегнуть к использованию предикатов, таких как isCNFFormula, то я мог бы в первую очередь использовать простой рекурсивный тип данных, поскольку я не мог бы определить структуру Formula по его типу (в этом весь смысл, не так ли?) . Что мне не хватает? И кстати: спасибо за вашу помощь - я уже кое-что узнал из статьи и поэкспериментировал с описанной в ней концепцией.   -  person Till Theis    schedule 02.05.2011


Ответы (5)


преобразует экземпляр класса супертипа в один из классов подтипа

Классы типов Haskell так не работают.

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

Для моделирования таких языков выражений наиболее предпочтительным вариантом являются алгебраические типы данных (или иногда обобщенные алгебраические типы данных):

data Proposition
    = LITERAL Bool
    | ALL (Set Proposition)
    | ANY (Set Proposition)
    | NOT Proposition

которые можно сделать произвольно выразительными с помощью параметризованных типов или GADT, в зависимости от вашего приложения.

person Don Stewart    schedule 27.04.2011

Я сделал это ответом, потому что он довольно длинный, и я хотел отформатировать. На самом деле, я бы посчитал это комментарием, поскольку это скорее мнение, чем решение.

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

Существуют способы достижения расширяемого синтаксиса в Haskell, например. стиль «Наконец-то без тегов» [1] или двухуровневые типы Шеарда и Пашалича[2].

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

[1] http://www.cs.rutgers.edu/~ccshan/tagless/jfp.pdf

[2] http://homepage.mac.com/pasalic/p2/papers/JfpPearl.pdf

person stephen tetley    schedule 28.04.2011
comment
Двухуровневый подход выглядит многообещающе, и я рассмотрю его более подробно. То, что описано в другой статье, кажется слишком сложным для моей цели. - person Till Theis; 29.04.2011

Проблема с вашим кодом заключается в том, что в formulaToSubFormula _ = Atom вывод создается с помощью конструктора Atom, поэтому он всегда имеет тип Atom, тогда как сигнатура типа объявляет его любым типом с экземпляром SubFormula. Один из вариантов — добавить функцию в класс SubFormula:

class SubFormula a where
  atom :: a

instance SubFormula Atom where
  atom = Atom

formulaToSubFormula :: (Formula a, SubFormula b) => a -> b
formulaToSubFormula _ = atom

Конечно, если у вас будет только один экземпляр подтипа, вы можете полностью обойтись без класса:

formulaToSubFormula2 :: Formula a => a -> Atom

Также обратите внимание, что

data (Formula a) => Negation a = Negation a

вероятно, не делает то, что вы хотите. Предполагается, что только типы Formula a могут быть инвертированы и всегда будут иметь доступный контекст Formula, но вместо этого это означает, что каждый раз, когда вы используете Negation a, вам нужно будет предоставить контекст Formula a, даже если он не используется. Вы можете получить желаемый результат, написав это с помощью Синтаксис GADT:

data Negation a where
  Negation :: Formula a => a -> Negation a
person John L    schedule 27.04.2011

Здесь может происходить много вещей, и я сомневаюсь, что что-то связано с введением классов типов. (Причудливая вещь, которая может быть в ближайшем будущем, - это GADT.) Следующее очень просто; это просто предназначено для того, чтобы вы сказали, что вы хотите, более четко....

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

data Formula a = Atom a 
               | Negation (Formula a)    
               | Conjunction (Formula a) (Formula a) deriving (Show, Eq, Ord)

Вот функция, которая извлекает все подформулы:

subformulas (Atom a) = [Atom a]
subformulas (Negation p) = Negation p : subformulas p
subformulas (Conjunction p q) = Conjunction p q : (subformulas p ++ subformulas q)

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

data Atoms = P | Q | R | S | T | U | V deriving (Show, Eq, Ord)

Вот несколько случайных помощников:

k p q = Conjunction p q
n p  = Negation p
p = Atom P
q = Atom Q
r = Atom R
s = Atom S

x --> y = n $ k x (n y)  -- note lame syntax highlighting

-- Main>  ((p --> q) --> q)
-- Negation (Conjunction (Negation (Conjunction (Atom P) (Negation (Atom Q)))) (Negation (Atom Q)))
-- Main> subformulas ((p --> q) --> q)
-- [Negation (Conjunction (Negation (Conjunction (Atom P) (Negation (Atom Q)))) (Negation (Atom Q))),
--     Conjunction (Negation (Conjunction (Atom P) (Negation (Atom Q)))) (Negation (Atom Q)),
--     Negation (Conjunction (Atom P) (Negation (Atom Q))),
--     Conjunction (Atom P) (Negation (Atom Q)),Atom P,
--     Negation (Atom Q),Atom Q,Negation (Atom Q),Atom Q]

Давайте сделаем логические атомы!:

t = Atom True
f = Atom False

-- Main> t --> f
-- Main> subformulas ( t --> f)
-- [Negation (Conjunction (Atom True) (Negation (Atom False))),
--           Conjunction (Atom True) (Negation (Atom False)),      
--            Atom True,Negation (Atom False),Atom False]

Почему бы не использовать простую функцию оценки?

 eval :: Formula Bool -> Bool
 eval (Atom p) = p
 eval (Negation p) = not (eval p)
 eval (Conjunction p q) = eval p && eval q

несколько случайных результатов:

 -- Main> eval (t --> f )
 -- False
 -- Main> map eval $ subformulas (t --> f)
 -- [False,True,True,True,False]

Добавлено позже: обратите внимание, что Formula — это Functor с очевидным экземпляром, который может быть выведен GHC, если вы добавите Functor в производное предложение и языковую прагму {-#LANGUAGE DeriveFunctor#-}. Затем вы можете использовать любую функцию f :: a -> Bool в качестве присвоения значений истинности:

-- *Main> let f p = p == P || p == R
-- *Main>  fmap f (p --> q)
-- Negation (Conjunction (Atom True) (Negation (Atom False)))
-- *Main> eval it
-- False
-- *Main>  fmap f ((p --> q) --> r)
-- Negation (Conjunction (Negation (Conjunction (Atom True) (Negation (Atom False)))) (Negation (Atom True)))
-- *Main> eval it
-- True
person applicative    schedule 27.04.2011

Единственный способ, который я нашел для представления ограничений вложенности в типах данных, — это какая-то система правил через классы типов, подобные этому:

data Formula t val = Formula val deriving Show

-- formulae of type t allow negation of values of type a

class Negatable t a
instance Negatable Fancy a
instance Negatable Normal a
instance Negatable NNF Atom
instance Negatable CNF Atom
instance Negatable DNF Atom

class Conjunctable t a
instance Conjunctable Fancy a
instance Conjunctable Normal a
instance Conjunctable NNF a
instance Conjunctable CNF a
instance Conjunctable DNF Atom
instance Conjunctable DNF (Negation a)
instance Conjunctable DNF (Conjunction a b)

---

negate :: (Negatable t a) => Formula t a -> Formula t (Negation a)
negate (Formula x) = Formula $ Negation x

conjunct :: (Conjunctable t a, Conjunctable t b)
         => Formula t a -> Formula t b -> Formula t (Conjunction a b)
conjunct (Formula x) (Formula y) = Formula $ Conjunction x y

Документы, которые вы упомянули, особенно типы данных a la cart, были действительно полезны, хотя.

person Till Theis    schedule 08.05.2011