简体   繁体   中英

Type Class From Subset Of Recursive Type Class (Or Type From Recursive Type)

How do I create a recursive type class which behaves like another recursive type class but has not as many instances as the "parent" class?

Here is an example:

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

That code compiles just fine but when I add a function which converts an instance of the super type class to one of the sub type class

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

I get an error

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

My original intent was to do this with normal types but with type classes the problem seems more approachable and generally more flexible.

For example:

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

Edit

To clarify what I try to achieve: I want to verify on the type level that an operation on the input type will return only a subset of that type as the result.

Extended example (propositional logic; no valid Haskell syntax):

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

At a later point I may have a formula in the conjunctive normal form (no valid Haskell syntax)

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

Therefore I need a tool to represent this hierarchy.

converts an instance of the super type class to one of the sub type class

Haskell typeclasses don't work like this.

They don't provide coercions or subtyping. Your function returning an Atom can only be of Atom return type, since it returns an explicit constructor that builds Atom values.

For modelling expression languages like this, algebraic data types (or sometimes, generalized algebraic data types ) are the overwhelmingly preferred option:

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

which can be made arbitrarily expressive with parameterized types, or GADTs, depending on your application.

I've made this an answer because it's quite long and I wanted formatting. Really, I'd consider it a comment as it's more of an opinion than a solution.

It looks like you are wanting extensible / modular syntax although you are phrasing your needs from the general to the specific - most writing about extensible syntax takes the other view and considers adding extra cases to make "small" syntax larger.

There are ways to achieve extensible syntax in Haskell eg the "Finally Tagless" style [1] or Sheard and Pasalic's two level types[2].

In practice, though, the "protocol" code to get modular syntax is complicated and repetitive and you lose nice features of regular Haskell data types, particularly pattern matching. You also lose a lot of clarity. This last bit is crucial - modular syntax is such a "tax" on clarity that it is rarely worth using. You are usually better off using data types that exactly match your current problem, if you need to extend them later you can edit the source code.

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

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

The problem with your code is that in formulaToSubFormula _ = Atom , the output is created with the Atom constructor, so it is always of type Atom , whereas the type signature declares it to be any type with a SubFormula instance. One option is to add a function to the SubFormula class:

class SubFormula a where
  atom :: a

instance SubFormula Atom where
  atom = Atom

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

Of course, if you will only have one instance of the subtype, you can dispense with the class entirely:

formulaToSubFormula2 :: Formula a => a -> Atom

Also note that

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

probably doesn't do what you want. The intention is presumably that only Formula a types can be negated and will always have the Formula context available, but instead this means that any time you use a Negation a you will need to provide a Formula a context, even if it isn't used. You can get the desired result by writing this with GADT syntax :

data Negation a where
  Negation :: Formula a => a -> Negation a

There are many things that might be going on here, I doubt that any involves the introduction of type classes. (The fancy thing that might be in the offing here is a GADT.) The following is very simple-minded; it is just intended to get you to say what you want more clearly....

Here is a polymorphic type like the one you had originally. Since it is polymorphic you can use anything to represent the atomic formulas.

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

Here is a function that extracts all subformulas:

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

Here is a type to use if you aren't contemplating very many atomic propositions:

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

Here are some random helpers:

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]

Lets make Boolean Atoms::

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]

Why not a simple evaluation function?

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

a few random results:

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

Added later: note that Formula is a Functor with an obvious instance that can be inferred by the GHC if you add Functor to the deriving clause and the language pragma {-#LANGUAGE DeriveFunctor#-} . Then you can use any function f:: a -> Bool as an assignment of truth values:

-- *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

The only way I found to represent the nesting constraints in the data types is some kind of rule system via type classes like this:

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

The papers you mentioned, especially Data types a la cart , were really helpful though.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM