Haskell data type

While learning Haskell, I am writing C ++ header file formatting. First, I parse all the members of the class into collection-of-class members, which are then passed to the formatting routine. For representing the members of a class, I have

data ClassMember = CmTypedef Typedef |
                   CmMethod Method |
                   CmOperatorOverload OperatorOverload |
                   CmVariable Variable |
                   CmFriendClass FriendClass |
                   CmDestructor Destructor

      

(I need to classify the members of a class this way due to some specific formatting style.)

The problem that annoys me is that in order to "drag" any function defined for class member types to a level ClassMember

, I have to write a lot of redundant code. For example,

instance Formattable ClassMember where
    format (CmTypedef td) = format td
    format (CmMethod m) = format m
    format (CmOperatorOverload oo) = format oo
    format (CmVariable v) = format v
    format (CmFriendClass fc) = format fc
    format (CmDestructor d) = format d

instance Prettifyable ClassMember where
    -- same story here

      

On the other hand, I would definitely like to have a list of objects ClassMember

(at least I think so), so defining it as

data ClassMember a = ClassMember a

instance Formattable ClassMember a
    format (ClassMember a) = format a

      

doesn't seem to be an option.

The alternatives I am considering are as follows:

  • Store in ClassMember

    non-object instances, but functions defined for the corresponding types that are needed in the formatting procedure. This approach violates modularity, IMO, since the analysis results presented [ClassMember]

    should be aware of all their usages.

  • Define ClassMember

    as an existential type, so [ClassMember]

    no longer a problem. I doubt this design is strict enough and, again, I need to specify all constraints in the definition, eg data ClassMember = forall a . Formattable a => ClassMember a

    . Also, I would prefer a solution without using extensions.

Am I doing the right way to do this in Haskell, or is there a better way?

+3


source to share


2 answers


First, consider reducing the size of your ADT. Operator overloads and destructors are special kinds of methods, so it makes sense to consider all three in CmMethod

; Method

will have special ways to separate them. Also, keep all three CmMethod

, CmOperatorOverload

and CmDestructor

, but let them all contain the same type Method

.

But of course, this is the only way you can reduce the complexity.

As for a specific instance example Show

: you really don't want to write this yourself, except in some special cases. For your case, it's much smarter to get an instance automatically:



data ClassMember = CmTypedef Typedef
                 | CmMethod Method
                 | ...
                 | CmDestructor Destructor
                 deriving (Show)

      

This will give different results from your custom instance - because yours is wrong: showing the contained result should also provide constructor information.

If you're not very interested in Show

, but talking about another class C

that does something more specific to ClassMember

- well, then you probably shouldn't have defined C

in the first place! The purpose of class classes is to express mathematical concepts that are stored for a wide variety of types.

+4


source


A possible solution is to use records. It can be used without extensions and remains flexible.

There is some boilerplate code, but you only need to type it once for everyone. Therefore, if you need to perform another set of operations on your ClassMember, it would be very easy and quick to do it.

Here's an example for your specific case (the Haskell and Control.Lens pattern makes it easy, but not necessary):



{-# LANGUAGE TemplateHaskell #-}

module Test.ClassMember

import Control.Lens

-- | The class member as initially defined.
data ClassMember =
      CmTypedef Typedef
    | CmMethod Method
    | CmOperatorOverload OperatorOverload
    | CmVariable Variable
    | CmFriendClass FriendClass
    | CmDestructor Destructor

-- | Some dummy definitions of the data types, so the code will compile.
data Typedef = Typedef
data Method = Method
data OperatorOverload = OperatorOverload
data Variable = Variable
data FriendClass = FriendClass
data Destructor = Destructor

{-|
A data type which defines one function per constructor.
Note the type a, which means that for a given Hanlder "a" all functions
must return "a" (as for a type class!).
-}
data Handler a = Handler
    {
      _handleType        :: Typedef -> a
    , _handleMethod      :: Method -> a
    , _handleOperator    :: OperatorOverload -> a
    , _handleVariable    :: Variable -> a
    , _handleFriendClass :: FriendClass -> a
    , _handleDestructor  :: Destructor -> a
    }

{-|
Here I am using lenses. This is not mandatory at all, but makes life easier.
This is also the reason of the TemplateHaskell language pragma above.
-}
makeLenses ''Handler

{-|
A function acting as a dispatcher (the boilerplate code!!!), telling which
function of the handler must be used for a given constructor.
-}
handle :: Handler a -> ClassMember -> a
handle handler member =
    case member of
        CmTypedef a          -> handler^.handleType $ a 
        CmMethod a           -> handler^.handleMethod $ a
        CmOperatorOverload a -> handler^.handleOperator $ a
        CmVariable a         -> handler^.handleVariable $ a
        CmFriendClass a      -> handler^.handleFriendClass $ a
        CmDestructor a)      -> handler^.handleDestructor $ a

{-|
A dummy format method.
I kept things simple here, but you could define much more complicated
functions.

You could even define some generic functions separately and... you could define
them with some extra arguments that you would only provide when building
the Handler! An (dummy!) example is the way the destructor function is
constructed.
-}
format :: Handler String
format = Handler
    (\x -> "type")
    (\x -> "method")
    (\x -> "operator")
    (\x -> "variable")
    (\x -> "Friend")
    (destructorFunc $ (++) "format ")

{-|
A dummy function showcasing partial application.
It has one more argument than handleDestructor. In practice you are free
to add as many as you wish as long as it ends with the expected type
(Destructor -> String).
-}
destructorFunc :: (String -> String) -> Destructor -> String
destructorFunc f _ = f "destructor"

{-|
Construction of the pretty handler which illustrates the reason why
using lens by keeping a nice and concise syntax.

The "&" is the backward operator and ".~" is the set operator.
All we do here is to change the functions of the handleType and the
handleDestructor.
-}
pretty :: Handler String
pretty = format & handleType       .~ (\x -> "Pretty type")
                & handleDestructor .~ (destructorFunc ((++) "Pretty "))

      

And now we can run some tests:

test1 = handle format (CmDestructor Destructor)
> "format destructor"

test2 = handle pretty (CmDestructor Destructor)
> "Pretty destructor"

      

0


source







All Articles