繁体   English   中英

Haskell中的数据类型设计

[英]Data type design in Haskell

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

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不是对象实例本身,而是在相应类型上定义的函数,这些函数是格式化例程所需的。 这种方法打破了模块化,IMO,因为[ClassMember]代表的解析结果需要了解它们的所有用法。

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

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

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

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

至于Show实例的具体示例:除了某些特殊情况外,你真的不想自己编写。 对于您的情况,自动派生实例更合理:

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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