在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不同)给出了第一个代码的警告,因为[]
- 案例没有被覆盖,但为了简洁起见,我可以忽略它而不破坏功能样式约定吗?一旦MatchFailure确实很好地陈述了问题...
答案 0 :(得分:45)
如果使用构造函数[]
完成模式匹配而不是全能_
,编译器将有机会告诉您再次查看该函数,并在当天发出警告有人在列表中添加了第三个构造函数。
我的同事和我,在一个大型OCaml项目(200,000多行)上工作,强迫我们避免部分模式匹配警告(即使这意味着不时写| ... -> assert false
)并避免如此 - 称为“fragile pattern-matchings”(模式匹配以这样的方式编写,即可能无法检测到构造函数的添加)。我们认为可维护性有益。
答案 1 :(得分:9)
显式优于隐式(借用Python的Zen;))
与在enum
上的C切换完全相同...最好编写所有情况(通过掉落)而不是仅仅放置default
,因为编译器将告诉您是否在枚举中添加了新元素,而忘记处理它们。
答案 2 :(得分:5)
我认为这在很大程度上取决于背景。您是在尝试编写健壮,易于调试的代码,还是在尝试编写简单易懂的内容?
如果我正在与多个开发人员一起开展长期项目,我会在断言中添加一个更有用的错误消息。我同意Pascal的评论,从软件工程的角度来看,不使用通配符是理想的。
如果我正在开展一个规模较小的项目,我是唯一的开发者,我不会再考虑使用不完整的匹配。如有必要,您可以随时检查编译器警告。
我认为这也取决于你所匹配的类型。实际上,没有额外的联合案例会添加到列表类型中,因此您不必担心脆弱的匹配。另一方面,在您控制并正在积极处理的代码中,可能存在不断变化的类型并且添加了额外的联合案例,这意味着防止脆弱匹配可能是值得的。
答案 3 :(得分:5)
这是一个更普遍的问题的特例,“你应该创造部分功能吗”。不完整模式匹配只是部分函数的一个例子。
通常,总函数是优选的。当你发现自己正在寻找一个只需要部分的功能时,问问自己是否可以先在类型系统中解决问题。有时这比它的价值更麻烦(例如,创建一个已知长度的整个类型的列表只是为了避免“head []”问题)。所以这是一个权衡。
或者你可能只是问一下在部分功能方面的优秀做法是否可以说
head [] = error "head: empty list"
在这种情况下答案是肯定的!
答案 4 :(得分:4)
Haskell前奏(标准函数)包含许多部分函数,例如头尾只能在非空列表上工作,但不要问我为什么。
答案 5 :(得分:2)
这个问题有两个方面。
由于这两方面相互矛盾,正确的答案取决于具体情况(另见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
答案 6 :(得分:-1)
非详尽的模式机制在Haskell中是惯用的,但在F#(和OCaml,标准ML等)中风格非常糟糕。
穷举检查是一种非常有价值的方法,可以在编译时以严格的语言(如F#)捕获错误,但是像Haskell这样的惰性语言经常使用无限的懒惰列表,其中空列表不会出现,因此它们(历史上)选择简洁而不是静态检查的正确性。