【问题标题】:Validations in HaskellHaskell 中的验证
【发布时间】:2012-01-04 03:16:16
【问题描述】:

我有一些需要验证的嵌套记录,我想知道 Haskell 的惯用方法是什么。

为了简化:

data Record = Record {
  recordItemsA :: [ItemA],
  recordItemB :: ItemB
} deriving (Show)

data ItemA {
  itemAItemsC :: [ItemC]
} deriving (Show)

要求是:

  • 收集并返回所有验证错误
  • 某些验证可能跨项目,例如ItemsA 反对 ItemB
  • Strings 足以表示错误

我目前的代码感觉很尴尬:

type ErrorMsg = String

validate :: Record -> [ErrorMsg]
validate record =
  recordValidations ++ itemAValidations ++ itemBValidations
  where
    recordValidations :: [ErrorMsg]
    recordValidations = ensure (...) $
      "Invalid combination: " ++ (show $ recordItemsA record) ++ " and " ++ (show $ recordItemsB record)
    itemAValidations :: [ErrorMsg]
    itemAValidations = concat $ map validateItemA $ recordItemsA record
    validateItemA :: ItemA -> [ErrorMsg]
    validateItemA itemA = ensure (...) $
      "Invalid itemA: " ++ (show itemA)
    itemBValidations :: [ErrorMsg]
    itemBValidations = validateItemB $ recordItemB record
    validateItemB :: ItemB -> [ErroMsg]
    validateItemB itemB = ensure (...) $
      "Invalid itemB: " ++ (show itemB)

ensure :: Bool -> ErrorMsg -> [ErrorMsg]
ensure b msg = if b then [] else [msg]

【问题讨论】:

  • 感谢您的建议,看起来很有趣。同一个项目使用 uu-parsinglib 进行解析,因此应用样式验证非常适合。
  • 这里的菜鸟问题:什么是 (...) 符号?
  • (...) 只是省略了无聊的部分,而不是一些花哨的运算符。
  • @MauricioScheffer 该链接不再有效 :( 编辑:我找到了,这是你的意思吗?hackage.haskell.org/package/Validation

标签: haskell


【解决方案1】:

你已经拥有的基本没问题,只是需要清理一下:

  • 子验证应该是顶级定义,因为它们相当复杂。 (顺便说一句,where 子句定义上的类型签名通常被省略。)
  • 缺乏一致的命名约定
  • 大量的(++)s 序列会变得丑陋 - 请改用concat(或者可能是unwords
  • 较小的格式怪癖(有一些多余的括号,concat . map fconcatMap f 等)

这一切的产物:

validateRecord :: Record -> [ErrorMsg]
validateRecord record = concat
  [ ensure (...) . concat $
      [ "Invalid combination: ", show (recordItemsA record)
      , " and ", show (recordItemB record)
      ]
  , concatMap validateItemA $ recordItemsA record
  , validateItemB $ recordItemB record
  ]

validateItemA :: ItemA -> [ErrorMsg]
validateItemA itemA = ensure (...) $ "Invalid itemA: " ++ show itemA

validateItemB :: ItemB -> [ErrorMsg]
validateItemB itemB = ensure (...) $ "Invalid itemB: " ++ show itemB

我认为这很不错。如果您不喜欢列表表示法,可以使用Writer [ErrorMsg] monad:

validateRecord :: Record -> Writer [ErrorMsg] ()
validateRecord record = do
  ensure (...) . concat $
    [ "Invalid combination: ", show (recordItemsA record)
    , " and ", show (recordItemB record)
    ]
  mapM_ validateItemA $ recordItemsA record
  validateItemB $ recordItemB record

validateItemA :: ItemA -> Writer [ErrorMsg] ()
validateItemA itemA = ensure (...) $ "Invalid itemA: " ++ show itemA

validateItemB :: ItemB -> Writer [ErrorMsg] ()
validateItemB itemB = ensure (...) $ "Invalid itemB: " ++ show itemB

ensure :: Bool -> ErrorMsg -> Writer [ErrorMsg] ()
ensure b msg = unless b $ tell [msg]

【讨论】:

  • @pat:嗯,没错。我已经从我的回答中删除了陈述。
  • 您应该使用Data.Sequence 并将[ErrorMsg] 替换为(Seq ErrorMsg) 作为Monoid。然后,当Writer 完成后,您可以将Seq ErrorMsg 转换为[ErrorMsg]Data.Foldable.toList
  • A Seq 由于恒定因素可能并不理想,但此处的差异列表将是理想的。尽管如此,过早的优化和所有这些:)
  • 是的,你是对的;请参阅here 了解更多信息。差异列表包为here
【解决方案2】:

阅读the 8 ways to report errors in Haskell 文章。对于您的特定情况,由于您需要收集所有错误而不仅仅是第一个错误,@ehird 建议的 Writer monad 方法似乎最适合,但了解其他常见方法是件好事。

【讨论】:

    【解决方案3】:

    基于@ehird 的回答,您可以引入Validate 类型类:

    class Validate a where
      validate :: a -> [ErrorMsg]
    
    instance Validate a => Validate [a] where
      validate = concatMap validate
    
    instance Validate Record where
      validate record = concat
        [ ensure (...) . concat $
          [ "Invalid combination: ", show (recordItemsA record)
          , " and ", show (recordItemB record)
          ]
        , validate $ recordItemsA record
        , validate $ recordItemB record
        ]
    
    instance Validate ItemA where
      validate itemA = ensure (...) $ "Invalid itemA: " ++ show itemA
    
    instance Validate ItemB where
      validate itemB = ensure (...) $ "Invalid itemB: " ++ show itemB
    

    【讨论】:

    • 我认为这不一定是个好主意;普通函数让事情变得更简单,如果有两种不同类型的验证可以应用于单一类型,那么这就会失败。不过,提升到列表很聪明。
    • 是的,但不能对任何类型类提出相同的论点......?即,如果有两种不同类型的节目可以应用于单一类型怎么办?
    • 确实,这就是为什么我对使用类型类持保守态度 :) Show 有一个限制,即它基本上只是用于调试和快速破解,它的输出应该是语法上有效的 Haskell,最好应该是是语义上有效的 Haskell,其计算值等于传递给 show 的参数。大多数对“备用Show 实例”的期望都试图违背这些非正式的限制。这是关于权衡;例如不太希望在同一类型上使用两组数值函数,如果有的话,Num 的便利性远远超过了它。
    • 是的,我明白了。类型类的真正强大之处在于能够编写通用函数,将它们用作类型签名中的上下文。除了简单地将函数广播到子项的容器实例(如上面Validate 的列表实例),可能很难找到想要validate 一些数据的函数,只知道它是Validate 的一个实例。通常,这样的函数会知道数据的具体类型,这使得 typeclass 没有实际意义。
    【解决方案4】:

    您可能会考虑尝试的一件事是,与其事后验证您的数据,不如使用出色的 fclabels 包中的镜头作为您的数据接口(而不是模式匹配/类型构造函数),以确保您的数据始终正确的。

    查看支持失败here 的变体,并通过将对数据类型进行一些验证的setter 和getter 传递给lens 函数来构建您的镜头。

    如果您需要一些更复杂的错误报告或诸如此类,请查看Maybelens 变体的implementation,并根据抽象接口定义您的镜头。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-10
      • 2011-02-25
      • 1970-01-01
      • 2020-03-14
      • 2017-12-11
      • 2017-05-05
      相关资源
      最近更新 更多