在haskell中记录具有多个构造函数的类型

时间:2014-05-30 13:18:33

标签: 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的主题,书籍等时,似乎同时使用具有记录语法和许多构造函数的类型并不常见。

所以问题是:这是"模式"是不是haskell-way,为什么?如果是这样,如何避免呢?

P.S。从提议的布局中哪个更好,或者可能更具可读性?因为我无法在其他来源中找到任何示例和建议。

4 个答案:

答案 0 :(得分:11)

当事情开始变得复杂,分裂和征服。通过从较简单的组合创建复杂实体,而不是通过将所有功能组合到一个位置来创建复杂实体。事实证明,这是一般编程的最佳方法,而不仅仅是在Haskell中。

您的示例都可以从分离中受益。 E.g。

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

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

现在您已将BinaryUnary分开,并且您可以独立地为每个人编写专用函数。这些功能将更简单,更容易推理和维护。

您还可以将这些类型放在单独的模块中,这将解决字段名称冲突。 Block的最终API将是非常简单的模式匹配并转发到BinaryUnary的专门函数。

这种方法是可扩展的。无论您的实体或问题有多复杂,您都可以随意添加其他级别的分解。

答案 1 :(得分:8)

这种类型的一个问题是访问者的功能不再是完全的,这些天是相当不赞成的,这是有充分理由的。这可能就是他们在书中避免使用的原因。

IMO,多构造函数记录原则上仍然很好,只需要理解标签应该用作访问者函数。但它们仍然非常有用,尤其是RecordWildCards扩展名。

这些类型肯定存在于许多库中。当构造函数隐藏时,你绝对没问题。

答案 2 :(得分:4)

我认为访问者功能的偏好是一个主要缺点。但是,仅当我们不使用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来自镜头库,但这相当于增加了"重量"我们的解决方案

答案 3 :(得分:3)

我建议不要同时使用ADT和记录类型,只是因为unOp (Binary (+) "+" "1" "2" "3")类型在没有-Wall警告的情况下进行检查,但会导致程序崩溃。它本质上绕过了类型系统,我个人认为应该从GHC中删除该功能,或者你必须使每个构造函数具有相同的字段。

您想要的是两种记录的总和类型。这是完全可以实现的,Either更加安全,并且需要大约两倍的样板,因为您必须编写isBinaryOpisUnaryOp函数来镜像isLeft或{{ 1}}。此外,isRight有许多功能和实例,可以更轻松地使用它,而您的自定义类型则不然。只需将每个构造函数定义为自己的类型:

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 ... 不能仅限于