7

我想知道 Haskell 中最标准的方法是什么。

第一个明确指出我们需要两个参数(大多数时候)。

第二个涉及id在第二个子句中的函数调用 (),因此它的效率应该较低,因为在第一个实现中我们可以简单地返回第二个参数。

所以我倾向于认为第一个更好,应该是一个选择:更容易阅读并弄清楚它的作用[1],以及一个函数调用保存。

但我是 Haskell 的新手,也许编译器优化了这个额外的调用。

xor :: Bool -> Bool -> Bool

xor True x = not x
xor False x = x

xor True = not
xor False = id

另外,我想知道我是否可以在False那里用通配符替换两者。

那么,在 Haskell 中有什么好的做法。也许另一种实现?

[1] 我们省略了它是一个众所周知的功能,让我们想象它是一个非平凡的功能。

谢谢

4

3 回答 3

14

为了可读性,我会尽量避免模式匹配,并用一个方程来定义函数,该方程表达了关于要定义的函数的一些有趣的东西。这并不总是可能的,但对于这个例子,有很多选择:

  • xor = (/=)
  • xor a b = a /= b
  • xor a b = not (a == b)
  • xor a b = (a && not b) || (not a && b)
  • xor a b = (a || b) && not (a && b)
  • xor a b = odd (fromEnum a + fromEnum b)
于 2013-07-08T22:26:29.997 回答
12

当然,这取决于编译器和传递给编译器的选项。

对于这个特定的示例,如果您在没有优化的情况下进行编译,GHC 会按照您编写的代码生成代码,因此第二个版本包含对idresp 的调用。到not. 这比第一个版本效率略低,第一个版本只包含对 的调用not

Xors.xor1 :: GHC.Types.Bool -> GHC.Types.Bool -> GHC.Types.Bool
[GblId, Arity=2]
Xors.xor1 =
  \ (ds_dkm :: GHC.Types.Bool) (x_aeI :: GHC.Types.Bool) ->
    case ds_dkm of _ {
      GHC.Types.False -> x_aeI;
      GHC.Types.True -> GHC.Classes.not x_aeI
    }

Xors.xor2 :: GHC.Types.Bool -> GHC.Types.Bool -> GHC.Types.Bool
[GblId, Arity=1]
Xors.xor2 =
  \ (ds_dki :: GHC.Types.Bool) ->
    case ds_dki of _ {
      GHC.Types.False -> GHC.Base.id @ GHC.Types.Bool;
      GHC.Types.True -> GHC.Classes.not
    }

(调用仍在生成的程序集中,但核心更具可读性,所以我只发布)。

但是经过优化,两个函数都编译到同一个内核(然后编译到同一个机器代码),

Xors.xor2 =
  \ (ds_dkf :: GHC.Types.Bool) (eta_B1 :: GHC.Types.Bool) ->
    case ds_dkf of _ {
      GHC.Types.False -> eta_B1;
      GHC.Types.True ->
        case eta_B1 of _ {
          GHC.Types.False -> GHC.Types.True;
          GHC.Types.True -> GHC.Types.False
        }
    }

idGHC eta 扩展了第二个版本并内联了对and的调用not,你得到了纯粹的模式匹配。

无论有没有优化,第二个等式使用False或通配符在任何一个版本中都没有区别。

也许编译器优化了这个额外的调用。

如果您要求它进行优化,在像这样的简单情况下,GHC 将消除额外的调用。

让我们想象它是一个非平凡的函数。

这是一个可能的问题。如果代码不够简单,编译器可能无法消除通过定义未提供所有参数的函数而引入的所有调用。但是,GHC 非常擅长这样做并内联调用,因此您需要相当多的非平凡性才能使 GHC 无法消除对它在编译代码时知道的简单函数的调用(它当然永远不会内联对它不知道的函数的调用) '不知道编译有问题的模块时的实现)。

如果是关键代码,请始终检查编译器生成的代码,对于 GHC,相关标志是-ddump-simpl获取优化后生成的核心,并-ddump-asm获取生成的程序集。

于 2013-07-08T20:37:11.063 回答
4

所以我倾向于认为第一个更好,应该是一个选择:更容易阅读并弄清楚它的作用

我同意可读性。然而,第二个是非常惯用的 Haskell,对于有经验的程序员来说更容易阅读:不执行这种琐碎的 eta 减少是非常可疑的,实际上可能会分散意图。因此,对于优化版本,我宁愿以明确的形式完全写出来:

True  `xor` False = True
False `xor` True  = True
_     `xor` _     = False

但是,如果这样的替代方案的可读性远低于最惯用的替代方案,则您应该考虑不要替换它,而是添加提示,以便编译器仍然可以将其优化为理想版本。正如 Daniel Fischer 所证明的那样,GHC 本身就非常聪明,并且经常会在没有帮助的情况下完成它。如果没有,添加一些INLINE和/或RULES编译指示可能会有所帮助。弄清楚如何做到这一点以获得最佳性能并不容易,但编写快速的 Haskell98 代码也是如此。

于 2013-07-08T21:00:42.733 回答