signum
函数是通常mathematical definition of sign的实现,它有条件地返回{-1,0,1}中的值。这是一个理论定义,因此,它没有考虑操作的计算成本或值的数据类型,因此乘以(-1)是改变符号的零成本理论方法。因此,它不是最有用的编程符号处理方法。
案例signum a == 0
并不实用,因为您可以直接测试a == 0
,而无需计算signum a
的额外费用。至于其他两个值,我认为它们仅用于三种方式:
要么测试值是正数还是负数才能有条件地启动不同的代码,如:
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
是非常常见的情况(您可以看到这些运算符只是处理{{1} } True
和1
为False
):
-1
我们甚至可以将声明括在一个名为(+.) a b = a + 1 *. b
(-.) a b = a - 1 *. b
的类中,以便于使用,包括正确的签名和固定性。
这样,上面的一般例子不仅可以简化代码,还可以简化执行时间和空间,因为我们避免了乘法(改为使用Signed
),一旦我们得到{{{},我们就避免了额外的比较1}},我们可以从一种类型的数据中获取符号,并将其用于另一种类型,而无需进行类型转换,我们使用短类型(*.)
而不是可能长类型的类Bool
。但我们获得了更多的灵活性,同时允许对代码和数据类型进行一些优化。
那么,我的问题是,是否存在与此处公开的三个一般用例不同的情况,即此方法不容易涵盖的情况,当前Bool
函数对Bool有利的情况标志方法。更确切地说,我是否可以完全避免使用当前Num
函数而不会降低效率或代码清晰度?
编辑:在Reid Barton评论之后,我将第一段修改为更“中立”的方式。
进度更新:在当前答案和评论的帮助下,此方法的代码在简洁性和清晰度方面得到了极大的改进。
答案 0 :(得分:10)
您假设“正面”和“负面”是唯一两个可能的迹象。但对于例如Complex Double
,signum
操作返回一个具有相同“方向”但幅度为1的复数:
Data.Complex> signum (3 :+ 4)
0.6 :+ 0.8
答案 1 :(得分:4)
我使用了这样的函数,通过一系列向(正交和对角)相邻单元格的移动,将光标导航到方形网格中的目标偏移。
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
答案 2 :(得分:4)
解决时间复杂性的问题:
分支不是免费的,如果你必须(在概念上)将值乘以几个不同位置的相同值的符号结果,那么拥有let s = signum x in ...
或具有该绑定可能更有效。一个where
- 条款。您不再需要每次都经过分支机构。另请注意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
效率分析通常不像您预期的那样明确,并且可能高度依赖于特定程序的非常具体的方面,正如我在上面链接的问题中所见,因此在优化"之前引用了" profile的建议。在我链接的问题中,执行更多指令的代码版本实际上运行更快比执行更少指令的版本(我可以在我的机器上验证这些结果,即使我在分析中包含了额外的排序指令)!