首页 文章

Haskell中的数据类型设计

提问于
浏览
3

学习Haskell,我写了一个C头文件的格式化程序 . 首先,我将所有类成员解析为a-collection-of-class-members,然后将其传递给格式化例程 . 代表我有的 class 成员

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

似乎不是一个选择 .

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

  • 存储在 ClassMember 中不是对象实例本身,而是在相应类型上定义的函数,这些函数是格式化例程所需的 . 这种方法打破了模块化,IMO,因为 [ClassMember] 表示的解析结果需要知道它们的所有用法 .

  • ClassMember 定义为存在类型,因此 [ClassMember] 不再是问题 . 我怀疑这个设计是否足够严格,同样,我需要在定义中指定所有约束,如 data ClassMember = forall a . Formattable a => ClassMember a . 此外,我更喜欢不使用扩展的解决方案 .

我正在以正确的方式在Haskell中做到这一点还是有更好的方法?

2 回答

  • 0

    首先,考虑稍微削减ADT . 运算符重载和析构函数是一种特殊的方法,因此在 CmMethod 中处理所有三个方法可能更有意义; Method 将有特殊的方法来分开它们 . 或者,保留所有三个 CmMethodCmOperatorOverloadCmDestructor ,但让它们都包含相同的 Method 类型 .

    但是,当然,你可以减少这么多的复杂性 .

    至于 Show 实例的具体示例:您真的不需要自动导出实例更合理:

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

    这将给您的自定义实例提供不同的结果 - 因为您的错误:显示包含的结果还应该提供有关构造函数的信息 .

    如果你对 Show 不是真的感兴趣,但是在讨论另一个类 C ,它会对 ClassMember 做更具体的事情 - 那么你可能不应该首先定义 C !类型类的目的是表达适用于各种类型的数学概念 .

  • 4

    一种可能的解决方案是使用记录 . 它可以在没有扩展的情况下使用并保持灵活性 .

    仍然有一些样板代码,但您只需要输入一次 . 因此,如果您需要在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"
    

相关问题