如果我可以使用 "equations",为什么要使用大小写表达式?



我正在学习Haskell,来自"Real World Haskell"一书。 在第 66 页和第 67 页中,他们显示了以下示例中的 case 表达式:

fromMaybe defval wrapped =
case wrapped of 
Nothing    -> defval
Just value -> value

我记得在F#中也有类似的事情,但是(如本书前面所示)Haskell可以将函数定义为一系列方程;而AFAIK,F Sharp则不能。所以我试图以这样的方式定义它:

fromMaybe2 defval Nothing = defval
fromMaybe2 defval (Just value) = value

我把它加载到 GHCi 中,经过几个结果后,我说服自己它是相同的 然而; 这让我想知道,为什么在方程时应该有大小写表达式:

  • 更容易理解(这是数学;为什么要用case something of,谁说的?
  • 不那么冗长(2 行与 4 行);
  • 需要更少的结构和合成糖(->可能是运算符,看看他们做了什么!
  • 仅在需要时使用变量(在基本情况下,例如此wrapped仅占用空间)。

案例表达式有什么好处?它们的存在仅仅是因为类似的基于 FP 的语言(如 F#)拥有它们吗?我错过了什么吗?

编辑:

我从@freyrs的回答中看到,编译器使这些完全相同。因此,方程总是可以转换为大小写表达式(如预期的那样)。我的下一个问题是相反的;是否可以走与编译器相反的路线,并使用带有let/where表达式的方程来表达任何大小写表达式?

这来自拥有小型"内核">面向表达式语言的文化。Haskell源于Lisp的根源(即lambda演算和组合逻辑);它基本上是Lisp加上语法加上显式数据类型定义加上模式匹配减去突变加上惰性评估(惰性评估本身首先在Lisp AFAIK中描述;即在70-s中)。

类Lisp语言是面向表达式的,即一切都是表达式,语言的语义被赋予为一组归约规则,将更复杂的表达式转换为更简单的表达式,最终变成"值"。

方程不是表达式。几个方程可以以某种方式混合成一个表达式;您必须为此引入一些语法;case就是这种语法。

Haskell的丰富语法被翻译成更小的"核心"语言,case作为其基本构建块之一。case必须是一个基本的构造,因为Haskell中的模式匹配被设计成语言的基本核心特征。


对于您的新问题,是的,您可以通过引入路易斯·卡西利亚斯(Luis Casillas)在他的答案中显示的辅助功能或使用模式防护装置,因此他的示例变为:

foo x y | (Meh o p) <- z = baz y p o
| (Gah t q) <- z = quux x t q
where 
z = bar x

这两个函数在Haskell(称为Core)中编译成完全相同的内部代码,你可以通过将标志-ddump-simpl -dsuppress-all传递给ghc来转储。

变量名称可能看起来有点吓人,但它实际上只是您上面编写的代码的显式类型版本。唯一的区别是变量名称。

fromMaybe2
fromMaybe2 =
 @ t_aBC defval_aB6 ds_dCK ->
case ds_dCK of _ {
Nothing -> (defval_aB6) defval_aB6;
Just value_aB8 -> (value_aB8) value_aB8
}
fromMaybe
fromMaybe =
 @ t_aBJ defval_aB3 wrapped_aB4 ->
case wrapped_aB4 of _ {
Nothing -> (defval_aB3) defval_aB3;
Just value_aB5 -> (value_aB5) value_aB5
}

论文"A History of Haskell: Being Lazy with Class"(PDF)为这个问题提供了一些有用的观点。 第 4.4 节("声明样式与表达式样式",第 13 页)是关于这个主题的。 金钱报价:

[我们]就哪种风格"更好"进行了激烈的辩论。一个潜在的假设是,如果可能的话,应该有"一种方法来做某事",这样,例如,同时拥有letwhere将是多余和混乱的。[...]最后,我们放弃了底层假设,并为两种风格提供了完整的语法支持。

基本上他们无法就一个达成一致,所以他们把两个都扔了进去。 (请注意,引用明确地是关于letwhere的,但他们将这种选择和case与方程的选择视为同一基本选择的两种表现形式——他们称之为"声明风格"与"表达风格"。

在现代实践中,声明风格(您的"一系列方程")已成为更常见的一种。 在这种情况下经常看到case,您需要匹配从其中一个参数计算的值:

foo x y = case bar x of
Meh o p -> baz y p o
Gah t q -> quux x t q

您可以随时重写它以使用辅助函数:

foo x y = go (bar x)
where go (Meh o p) = baz y p o
go (Gah t q) = quux x t q

这有一个非常小的缺点,即您需要命名辅助函数 - 但在这种情况下go通常是一个完美的名称。

大小写表达式可以在任何需要表达式的地方使用,而方程不能。例:

1 + (case even 9 of True -> 2; _ -> 3)

你甚至可以嵌套大小写表达式,我已经看到了这样做的代码。但是,我倾向于远离大小写表达式,并尝试用方程解决问题,即使我必须使用where/let引入局部函数。

每个使用方程的定义都等效于一个用例。例如

negate True = False
negate False = True

代表

negate x = case x of
True -> False
False -> True

也就是说,这是表达同一事物的两种方式,前者由GHC翻译成后者。

从我读过的Haskell代码来看,尽可能使用第一种样式似乎是规范的。

参见Haskell'98报告的第4.4.3.1节。

您添加的问题的答案是肯定的,但它非常丑陋。

case exp of
pat1 -> e1
pat2 -> e2
etc.

我相信,可以效仿

let
f pat1 = e1
f pat2 = e2
etc.
in f exp

只要 f 在 Exp、E1、E2 等中不自由。但你不应该这样做,因为它很可怕。

最新更新