【问题标题】:In Functional Programming, is it considered a bad practice to have incomplete pattern matchings在函数式编程中,模式匹配不完整是否被认为是一种不好的做法
【发布时间】:2012-03-15 23:13:37
【问题描述】:

在 Haskell 或 F# 等函数式语言中使用非穷举模式处理通常被认为是一种不好的做法,这意味着指定的案例并未涵盖所有可能的输入案例?

特别是,我应该允许代码因MatchFailureException 等而失败,还是应该始终涵盖所有情况并在必要时明确抛出错误?

例子:

let head (x::xs) = x

或者

let head list = 
    match list with
    | x::xs -> x
    |    _  -> failwith "Applying head to an empty list"

F#(与 Haskell 不同)对第一个代码给出警告,因为 []-case 没有被覆盖,但是为了简洁起见,我可以在不破坏功能样式约定的情况下忽略它吗?毕竟,MatchFailure 确实很好地说明了问题......

【问题讨论】:

  • +1,尽管我非常喜欢函数式编程,但我确信我们需要避免函数式编程特有的代码气味。
  • GHC 有 -fwarn-incomplete-patterns 如果你愿意的话。
  • 请注意,这与函数式编程无关,而是特定于从 ML 家族派生的语言中提供的模式匹配样式。许多函数式语言无法检测模式匹配的穷举性(例如 Mathematica)。

标签: haskell f# functional-programming


【解决方案1】:

如果您使用构造函数 [] 而不是包罗万象的 _ 完成模式匹配,编译器将有机会告诉您在有人添加第三个时再次查看该函数并发出警告列表的构造函数。

我和我的同事在从事一个大型 OCaml 项目(超过 200,000 行),强迫自己避免部分模式匹配警告(即使这意味着不时写| ... -> assert false)并避免所谓的“ fragile pattern-matchings"(模式匹配的编写方式可能无法检测到构造函数的添加)。我们认为可维护性会带来好处。

【讨论】:

  • 啊,没想到脆弱的匹配——这确实是点+1
  • 是的,避免脆弱的模式真的很有价值。 Dario,您可能会在我的《OCaml for Scientists》(以及我随后的 F# 书籍)免费提供的第一章中欣赏关于此的“良好风格”部分:ffconsultancy.com/products/ocaml_for_scientists/chapter1.html
  • @Complicated 查看简历我只想给你 16,666
  • @CrisStringfellow 我喜欢我的声誉分数是 5 的倍数,所以我对你随机挑选的一个答案投了反对票。
【解决方案2】:

显式优于隐式(借自 Python 之禅;))

这与在 C 中切换 enum 完全相同...最好编写所有案例(失败)而不是仅仅放置 default,因为编译器会告诉你如果你向枚举中添加新元素而您忘记处理它们。

【讨论】:

    【解决方案3】:

    我认为这在很大程度上取决于上下文。您是在尝试编写健壮、易于调试的代码,还是在尝试编写简单明了的代码?

    如果我正在与多个开发人员一起进行长期项目,我会添加断言以提供更有用的错误消息。我也同意 Pascal 的评论,即从软件工程的角度来看,不使用通配符是理想的。

    如果我正在开发一个我是唯一开发人员的小规模项目,我不会三思而后行地使用不完全匹配。如有必要,您可以随时检查编译器警告。

    我认为这也取决于您匹配的类型。实际上,不会将额外的联合案例添加到列表类型中,因此您无需担心脆弱的匹配。另一方面,在您控制并积极处理的代码中,很可能存在不断变化的类型并添加了额外的联合案例,这意味着防止脆弱匹配可能是值得的。

    【讨论】:

      【解决方案4】:

      这是一个更普遍的问题的特例,即“您是否应该创建偏函数”。不完全的模式匹配只是偏函数的一个例子。

      一般来说,总函数是可取的。当您发现自己正在查看一个必须是部分的函数时,请先问问自己是否可以先解决类型系统中的问题。有时这比它的价值更麻烦(例如,创建一个已知长度的完整类型的列表只是为了避免“head []”问题)。所以这是一个权衡。

      或者,也许你只是问它在偏函数中是否有好的做法,比如

      head [] = error "head: empty list"
      

      在这种情况下,答案是肯定的!

      【讨论】:

        【解决方案5】:

        Haskell 前奏曲(标准函数)包含许多偏函数,例如head 和 tail 仅适用于非空列表,但不要问我为什么。

        【讨论】:

        • +1:我不明白你为什么没有评论反对票。我说了同样的话,得到了两个反对票......
        • 呃,你希望head 在一个空列表上做什么?它没有定义,应该给出一个错误。事实上,在我的 ghci 中,head [] 给出了“异常:Prelude.head:空列表”,而不是提到匹配失败,所以 Prelude 中的 head 可能会检查空列表:它模式匹配是详尽无遗的。
        • @ShreevatsaR:您可以将listToMaybe 作为默认值,而不是headhackage.haskell.org/packages/archive/base/latest/doc/html/…
        • 我认为 Haskellers 很清楚这一点,例如参见 book.realworldhaskell.org/read/… haskell.org/haskellwiki/… ,我什至记得看到有人提议从前奏中删除此类功能。
        【解决方案6】:

        这个问题有两个方面。

        1. 对于 API 的用户,failwith... 只是抛出一个 System.Exception,这是不具体的(因此有时被认为本身是一种不好的做法)。另一方面,隐式抛出的 MatchFailureException 可以使用类型测试模式专门捕获,因此是可取的。
        2. 对于实现代码的审阅者,failwith... 清楚地证明了实现者至少对可能的情况进行了一些思考,因此是可取的。

        由于这两个方面相互矛盾,因此正确答案取决于具体情况(另请参阅 kvb 的答案)。从任何角度来看都是 100%“正确”的解决方案必须

        • 明确处理每个案例,
        • 在必要时抛出特定异常,并
        • 清楚地记录异常

        例子:

        /// <summary>Gets the first element of the list.</summary>
        /// <exception cref="ArgumentException">The list is empty.</exception>
        let head list = 
            match list with
            | [] -> invalidArg "list" "The list is empty." 
            | x::xs -> x
        

        【讨论】:

          猜你喜欢
          • 2015-10-29
          • 2014-04-20
          • 2017-07-28
          • 1970-01-01
          • 1970-01-01
          • 2021-09-26
          • 2022-01-09
          • 2017-12-18
          相关资源
          最近更新 更多