当我使用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。从提议的布局中哪个更好,或者可能更具可读性?因为我无法在其他来源中找到任何示例和建议。
答案 0 :(得分:11)
当事情开始变得复杂,分裂和征服。通过从较简单的组合创建复杂实体,而不是通过将所有功能组合到一个位置来创建复杂实体。事实证明,这是一般编程的最佳方法,而不仅仅是在Haskell中。
您的示例都可以从分离中受益。 E.g。
data Block a = BinaryBlock (Binary a) | UnaryBlock (Unary a)
data Binary a = Binary {
...
}
data Unary = Unary {
...
}
现在您已将Binary
和Unary
分开,并且您可以独立地为每个人编写专用函数。这些功能将更简单,更容易推理和维护。
您还可以将这些类型放在单独的模块中,这将解决字段名称冲突。 Block
的最终API将是非常简单的模式匹配并转发到Binary
和Unary
的专门函数。
这种方法是可扩展的。无论您的实体或问题有多复杂,您都可以随意添加其他级别的分解。
答案 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
礼物。
此外,拆分变体的问题是常见的字段名称会发生冲突。对于镜头,我们可能会有很长的非碰撞字段名称,然后使用简单的镜头名称通过类型类重载,或者makeClassy
和makeFields
来自镜头库,但这相当于增加了"重量"我们的解决方案
答案 3 :(得分:3)
我建议不要同时使用ADT和记录类型,只是因为unOp (Binary (+) "+" "1" "2" "3")
类型在没有-Wall
警告的情况下进行检查,但会导致程序崩溃。它本质上绕过了类型系统,我个人认为应该从GHC中删除该功能,或者你必须使每个构造函数具有相同的字段。
您想要的是两种记录的总和类型。这是完全可以实现的,Either
更加安全,并且需要大约两倍的样板,因为您必须编写isBinaryOp
和isUnaryOp
函数来镜像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
...
不能仅限于