簡體   English   中英

在haskell中記錄具有多個構造函數的類型

[英]Record types with multiple constructors in haskell

通常,當我使用Haskell編寫內容時,我需要具有多個構造函數的記錄。 例如,我想開發某種邏輯方案建模。 我想到了這樣的類型:

data Block a = Binary {
      binOp  :: a -> a -> a
    , opName :: String
    , in1 :: String
    , in2 :: String
    , out :: String
} | Unary {
      unOp  :: a -> a
    , opName :: String
    , in_ :: String
    , out :: String
}

它描述了兩種類型的塊:二進制(如和,或等)和一元(如不是)。 它們包含核心功能,輸入和輸出信號。

另一個例子:類型來描述控制台命令。

data Command = Command { info :: CommandInfo
                       , action :: Args -> Action () }
             | FileCommand { info :: CommandInfo
                           , fileAction :: F.File -> Args -> Action ()
                           , permissions :: F.Permissions}

FileCommand需要額外的字段 - 所需權限及其操作接受文件作為第一個參數。

當我閱讀和搜索關於Haskell的主題,書籍等時,似乎同時使用具有記錄語法和許多構造函數的類型並不常見。

所以問題是:這種“模式”是不是有問題,為什么? 如果是這樣,如何避免呢?

PS哪個來自提議的布局更好,或者可能有更多可讀的? 因為我在其他來源中找不到任何示例和建議。

當事情開始變得復雜,分裂和征服。 通過從較簡單的組合創建復雜實體,而不是通過將所有功能組合到一個位置來創建復雜實體。 事實證明,這是一般編程的最佳方法,而不僅僅是在Haskell中。

您的示例都可以從分離中受益。 例如

data Block a = BinaryBlock (Binary a) | UnaryBlock (Unary a)

data Binary a = Binary {
  ...
}
data Unary = Unary {
  ...
}

現在你將BinaryUnary分開,你就可以獨立地為每個函數編寫專用函數。 這些功能將更簡單,更容易推理和維護。

您還可以將這些類型放在單獨的模塊中,這將解決字段名稱沖突。 Block的最終API將是關於非常簡單的模式匹配並轉發到BinaryUnary專用函數。

這種方法是可擴展的。 無論您的實體或問題有多復雜,您都可以隨意添加其他級別的分解。

這種類型的一個問題是訪問器功能不再是完全的,這在當天是相當不贊成的,這是有充分理由的。 這可能就是他們在書中避免的原因。

IMO,多構造函數記錄原則上仍然很好,只需要理解標簽應該用作訪問器函數。 但它們仍然非常有用,尤其是RecordWildCards擴展。

在許多圖書館中都可以找到這種類型。 當構造者被隱藏時,你絕對是好的。

我認為訪問器功能的偏好是一個主要缺點。 然而,這只是我們不使用lens 有了它,它更舒適:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Block a = ...

makeLenses ''Block
makePrisms ''Block

現在完全消除了偏好:生成的訪問器明確是部分或全部(換句話說,1目標鏡頭或0目標遍歷):

block1 = Binary (+) "a" "b" "c" "d"
block2 = Unary id "a" "b" "x"

main = do
    print $ block1^. opName -- total accessor
    print $ block2^? in2    -- partial accessor, prints Nothing 

當然,我們得到了所有其他lens好東西。

此外,拆分變體的問題是常見的字段名稱將發生沖突。 使用鏡頭,我們可以使用長的非碰撞字段名稱,然后使用通過類型類重載的簡單鏡頭名稱,或者使用鏡頭庫中的makeClassymakeFields ,但這相當於我們解決方案“重量”的增加。

我建議不要同時使用ADT和記錄類型,只是因為unOp (Binary (+) "+" "1" "2" "3")類型檢查沒有警告-Wall ,但會崩潰你的程序。 它本質上繞過了類型系統,我個人認為應該從GHC中刪除該功能,或者你必須使每個構造函數具有相同的字段。

你想要的是兩種記錄的總和類型。 這是完全可以實現的,並與安全得多Either ,需要一樣多的樣板,因為你必須寫isBinaryOpisUnaryOp反正職能鏡isLeftisRight 此外, Either具有許多功能和實例,使其更容易使用,而您的自定義類型則不然。 只需將每個構造函數定義為自己的類型:

data BinaryOp a = BinaryOp
    { binOp :: a -> a -> a
    , opName :: String
    , in1 :: String
    , in2 :: String
    , out :: String
    }
data UnaryOp a = UnaryOp
    { unOp :: a -> a
    , opName :: String
    , in_ :: String
    , out :: String
    }

type Block a = Either (BinaryOp a) (UnaryOp a)

data Command' = Command
    { info :: CommandInfo
    , action :: Args -> Action ()
    }
data FileCommand = FileCommand
    { fileAction :: F.File -> Args -> Action ()
    , permissions :: F.Permissions
    }

type Command = Either Command' FileCommand

這不是真正的代碼,它與原始類型同構,同時充分利用了類型系統和可用功能。 您還可以輕松地在兩者之間編寫等效函數:

-- Before
accessBinOp :: (Block a -> b) -> Block a -> Maybe b
accessBinOp f b@(BinaryOp _ _ _ _ _) = Just $ f b
accessBinOp f _ = Nothing

-- After
accessBinOp :: (BinaryOp a -> b) -> Block a -> Maybe b
accessBinOp f (Left b) = Just $ f b
accessBinOp f _ = Nothing

-- Usage of the before version
> accessBinOp in1 (BinaryOp (+) "+" "1" "2" "3")
Just "1"
> accessBinOp in_ (BinaryOp (+) "+" "1" "2" "3")
*** Exception: No match in record selector in_
-- Usage of the after version
> accessBinOp in1 (Left $ BinaryOp (+) "+" "1" "2" "3")
Just "1"
> accessBinOp in_ (Left $ BinaryOp (+) "+" "1" "2" "3")
Couldn't match type `UnaryOp a1` with `BinaryOp a0`
Expected type: BinaryOp a0 -> String
  Actual type: UnaryOp a1 -> String
...

因此,如果您使用非全局函數,則會出現異常,但之后您只有總函數並且可以限制訪問器,以便類型系統為您捕獲錯誤,而不是運行時。

一個關鍵的區別是f不能僅限於工作

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM