Haskell中有序数字类型的“signum”用例

Posted

技术标签:

【中文标题】Haskell中有序数字类型的“signum”用例【英文标题】:Use cases of 'signum' for ordered numeric types in Haskell 【发布时间】:2017-01-31 00:10:39 【问题描述】:

signum 函数是通常的mathematical definition of sign 的实现,它有条件地返回 -1,0,1 中的值。这是一个理论定义,因此,它没有考虑操作的计算成本或值的数据类型,因此乘以 (-1) 是改变符号的零成本理论方法。因此,它不是编程中最有用的符号处理。

signum a == 0 的情况并不是很有用,因为您始终可以直接测试a == 0,而无需计算signum a 的额外成本。至于其他两个值,我认为它们仅用于 3 种通用方式:

您可以测试一个值是正还是负以有条件地启动不同的代码,如:

f x y | signum x == -1  = h x y
      | otherwise       = g x y

或者你在操作之前乘以1-1,如:

f x y = g x (y * b) where 
      b = signum x

或者你在操作之前添加1-1,如:

f x y = g x (y + b) where 
      b = signum x

在所有情况下,最好使用Bool 值作为符号。因此,我们只需要函数将Num 分解为绝对值和布尔符号,以及一个逆函数,它根据布尔条件(表示符号)改变值的符号。这个函数相当于将1-1乘以一个数字,所以我们将其定义为类似于(*)的运算符。 :

sgn  a      = a >= 0
(*.) a True = a
(*.) a _    = -a
abs  a      = a *. sgn a
signum1 a   = 1 *. sgn a

我添加了signum 的二分变体,它只能返回“-1,1”。请注意,在它前面加上signum 0 = 0 我们会得到通常的signum 函数,但我认为第三种情况通常没有用处。

我们可以类似地编写加法运算符,因为根据某事物的符号添加1-1 是非常常见的情况(您可以看到这些运算符只是将True 视为1False as -1):

(+.) a b    = a + 1 *. b
(-.) a b    = a - 1 *. b

我们甚至可以将声明包含在一个名为 Signed 的类中,以便于使用,包括正确的签名和固定性。

这样,上面的一般示例不仅在代码上,而且在执行时间和空间上都可以简化,因为我们避免了乘法(使用(*.)代替),一旦有了Bool,我们就避免了额外的比较,我们可以从一种类型的数据中获取符号并将其用于另一种类型而无需类型转换,并且我们使用短类型Bool 而不是可能的长类型类Num。但是我们获得了更大的灵活性,同时允许对代码和数据类型进行一些优化。

然后,我的问题是,是否存在与此处公开的三个一般用例不同的情况,即这种方法不容易涵盖的情况,当前signum 函数优于布尔符号方法的情况.更准确地说,我可以完全避免使用当前的signum 函数而不损失效率或代码清晰度吗?


编辑:根据 Reid Barton 的评论,我将第一段修改为更“中性”的方式。


进度更新:在当前答案和 cmets 的大力帮助下,为了简单明了,大大改进了此方法的代码。

【问题讨论】:

在我看来,零只是一种不同的情况。你不能说零的符号是“+”。从-0 == +0 开始,它同时是“+”和“-”。 (不,我没有 dv)。 我认为这个问题可以以更“中性”的方式提出。 Haskell 2010 报告只要求abs x * signum x == x;它没有说明signum 的共域的大小。 假设这只是“学术数学家的定义,他们不担心运算的计算成本”,那么为什么 Java 会做同样的事情呢?对于像 int 这样的类型,将它们保留在堆栈中(即使您将其相乘)通常比因额外盒装类型的间接导致缓存未命中更有效。 好吧,如果你不需要 3 个值而只需要 2 个,你可以使用 (>=0) 来代替你的布尔值。我不确定我们是否需要为此修改签名。如果你真的因为性能问题有特殊需求,那么滚动你自己的函数是可以接受的。 【参考方案1】:

您假设“积极”和“消极”是仅有的两个可能的迹象。但是对于例如Complex Doublesignum 操作返回一个“方向”相同但幅度为 1 的复数:

Data.Complex> signum (3 :+ 4)
0.6 :+ 0.8

【讨论】:

是的,但这提出了为什么将其称为“signum”的问题。可以说这个操作应该被称为normalise或类似的东西,它不应该将0映射到0(因为它没有单位模数)。 @leftaroundabout 我更喜欢较短的norm。除此之外,sgn 函数是一个thing in math,它通常在复数中的泛化分配为sgn(0) = 0 看来我的方法只适用于“签名”类型;-)。其实从签名上就可以看出,它只针对有序类型。但是好的,我正在处理的不适用于广义符号函数(这就是为什么我使用另一个名称 sgn 来表示布尔符号)。谢谢你,你的cmets和回答帮助了我。【参考方案2】:

我使用这样的函数通过一系列移动到(正交和对角)相邻单元格来将光标导航到方形网格中的目标偏移量。

move :: (Int, Int) -> [(Int, Int)]
move (0, 0) = []
move (x, y) = (dx, dy) : move (x - dx, y - dy)
  where dx = signum x
        dy = signum y

【讨论】:

很好的例子。但是您在每次递归时都在计算 signum,这是多余的,因为符号相同但不为零。对于较大的输入,您最好尝试预先计算递归符号以及abs xabs y,并处理其中一个坐标为零的情况。然后,signum 在这种情况下的明显优势是值得怀疑的(我认为不是)。 @enrique 如果您对代码进行分析并发现这会导致您正在运行它的输入出现很大的瓶颈,那么您可以避免重新计算by using the common Haskell idiom of a "go" function。不过,我不确定为什么需要在这里计算绝对值。 @David,您的代码不完整,因为当另一个坐标不为零时,它需要一个坐标变为零的单独情况。但你是对的,如果我们使用signum,我们就不需要abs。这个例子很好。 Here 是这个例子的优化版本,使用signum,基于不正确的@David solution,使用我的方法。您可以看到第一个解决方案并没有真正使用来自signum 的返回值0,因为我们都需要明确的代码行来处理只有一个坐标变为零的情况。【参考方案3】:

解决时间复杂度问题:

分支不是免费的,如果您必须(在概念上)将值乘以多个不同位置的相同值的符号结果,则使用 let s = signum x in ... 或在 @ 中使用该绑定可能会更有效987654323@-条款。您不再需要每次都经过一个分支。还要记住in some cases, code can slow down due to branching behavior that goes against what the branch predictor expects。

例如,假设您有这样的代码:

f x y z = (s * y) / (1 + (s * z))
  where
    s = signum x

效率分析通常不像您预期​​的那样明确,并且可能高度依赖于特定程序的非常具体的方面,正如我在上面链接的问题中所见,因此经常引用“优化前的配置文件”的建议。在我链接到的问题中,执行更多指令的代码版本实际上比执行更少指令的版本运行更快(我可以在我的机器上验证这些结果,即使我在分析中包含了额外的排序指令)!

【讨论】:

x 的类型与您的示例中的 yz 的类型不同呢?在我提出的方法中,这不是问题,因为它们都必须是Num 的实例(并且x 的类型是Ord 的实例)。我的意思是,效率不是唯一的,也不是主要的问题,它是一致性。 @enrique 我不确定我明白你的意思。最终,如果您要在它们上使用*/+,则事物必须是相同的类型,例如这样的示例。如果它们是不同的类型,您将需要任何一种方法中的转换函数。 signum 的存在与否不会改变这一点。你也可以通过在任何地方使用signum来保持一致...... 我认为signum 在概念上也更简单,因为它只需要一个非常简单的函数。只需要记住一个函数名称,很清楚哪些结果对应于正数和负数(更容易忘记True 的含义)和零,您可以直接将它与+ 等其他数学运算一起使用而无需某种排序if,守卫或case。顺便说一句,零既不是负数也不是正数,那么Bool 应该是哪个值?你应该如何记住这一点? setSgn s y 视为重载的(*) s y,其中sBooly 可以是任何类型的类Num。所以,你不需要转换。如果在 Haskell 中可以实现临时多态性,我将调用该函数 (*)。对我来说,概念上的简单性是 signumabs 实际上应该是同一操作的两个面(不是,请参阅来源),将任何数字分解为两者(也为零),以类似的方式quotRem,这样如果你需要signumabs,就不需要调用两个函数,这是一种常见的情况。 应该还有一个组合操作,与分解相反(这是(*),好的,尽管不是多态的)。通常,signum 被用作条件,这是认为作为 Bool 值会方便的主要动机,加上符号信息仅包含在有符号类型的一位中的事实。此外,*,+1,-1 是一个循环群,而*,+1,0,-1 不是。因此,最后,不适合的主要因素是零值,因为在对 Daniel 答案的评论中正确指向左侧。

以上是关于Haskell中有序数字类型的“signum”用例的主要内容,如果未能解决你的问题,请参考以下文章

scala简单的功能实现~weekone

如何优化 Haskell 代码以通过 HackerRanks 超时测试用例(不是为了任何正在进行的比赛,只是我在练习)

来自元组的函数 - Haskell

Haskell中是否有“继续”?

C/C++ 中是不是有标准的符号函数(signum、sgn)?

Haskell中不同类型之间的关系