簡體   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