Haskell中的数据类型设计

3

我正在学习Haskell,我编写了一个C++头文件的格式化程序。首先,我将所有类成员解析为类成员集合,然后将其传递给格式化程序。为了表示类成员,我有:

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

因为某些格式样式的特殊性,我需要以这种方式分类类成员。

令我烦恼的问题是,要将定义为类成员类型的任何函数“拖放”到ClassMember级别,我必须编写大量冗余代码。例如:

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

另一方面,我肯定希望有一个 ClassMember 对象列表(至少我是这么认为的),因此将其定义为:

data ClassMember a = ClassMember a

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

似乎没有这个选项。

我正在考虑的替代方案有:

  1. ClassMember 中存储对应类型上定义的函数,而不是对象实例本身,这些函数是格式化程序所需的。我认为这种方法会破坏模块化,因为表示为 [ClassMember] 的解析结果需要意识到它们的所有用途。

  2. ClassMember 定义为存在类型,因此 [ClassMember] 不再是问题。我怀疑这种设计是否足够严格,并且我需要在定义中指定所有约束条件,例如 data ClassMember = forall a . Formattable a => ClassMember a。另外,我希望找到一种不使用扩展的解决方案。

我现在采用的方式在 Haskell 中是否正确,还是有更好的方法?


1
你为什么需要首先使用FormattablePrettifyable类型类?你会格式化不是ClassMember的东西吗? - Benjamin Hodgson
@BenjaminHodgson:是的,这就是问题所在。需要格式化的实际内容是data FormattableItem = FiClassMember ClassMember (Maybe SingleLineComment) | FiSingleLineComment SingleLineComment | FiComment Comment | FiScopeModifier AccessModifier。格式化规则有点复杂:ClassMember对象由其他FormattableItem对象分组,并且在组内它们也相互依赖。 - AdelNick
2个回答

4
首先,考虑缩减ADT。运算符重载和析构函数是特殊类型的方法,因此将它们全部放在CmMethod中可能更有意义;反之,则可以让Method采取特殊方法来分离它们。或者,将这三个方法CmMethodCmOperatorOverloadCmDestructor都保留下来,但让它们包含相同的Method类型。
当然,你只能尽力降低复杂度。
至于Show实例的具体示例:除了在某些特殊情况下,你真的不想自己编写,否则更合理的做法是自动派生实例。
data ClassMember = CmTypedef Typedef
                 | CmMethod Method
                 | ...
                 | CmDestructor Destructor
                 deriving (Show)

这将产生与您自定义实例不同的结果——因为你的是错误的:显示一个包含的结果应该给出有关构造函数的信息。
如果你对“Show”不太感兴趣,而是在谈论另一个类“C”,这个类对“ClassMember”做一些更具体的事情——那么,在第一次定义“C”时,你可能就不应该这样做!类型类的目的是表达适用于许多类型的数学概念。

感谢您的评论。确实,Show 在这里很令人困惑,我将其更改为 Formattable。同时,我还对为什么需要所有这些构造函数进行了一些澄清。 - AdelNick

0
一种可能的解决方案是使用记录。 它可以在不使用扩展的情况下使用,并保持灵活性。
仍然有一些样板代码,但您只需要为所有内容输入一次即可。因此,如果您需要对ClassMember执行另一组操作,这将非常容易和快速。
以下是您特定情况的示例(模板Haskell和Control.Lens使事情更容易,但并非必需):
{-# 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 "))

现在我们可以运行一些测试:

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接