enrique
enrique

Reputation: 512

Use cases of 'signum' for ordered numeric types in Haskell

The signum function is the implementation of the usual mathematical definition of sign, which conditionally returns a value in {-1,0,1}. It is a theoretical definition, and as such, it doesn't take into account computational costs of operations, or data type of values, so that multiplying by (-1) is a zero cost theoretical way of changing sign. Because of that, it is not the most useful treatment of sign for programming.

The case signum a == 0 is not really useful, as you can always test directly a == 0, without the extra cost of computing signum a. As for the other two values, I think that they are used in just 3 general ways:

In all cases, it would be better a Bool value for sign. So, we just need functions to decompose a Num into absolute value and boolean sign, and an inverse function, which changes the sign of a value depending of a boolean condition (which represents a sign). This function is the equivalent of multiplying 1 or -1 by a number, so we define it as an operator similar to (*). :

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

I added a dichotomous variant of signum which can only return ´{-1,1}´. Notice that preceding it with signum 0 = 0 we would get the usual signum function, but that third case is what I think that is not generally useful.

We can code similarly an adding operator, because it is a very frequent case to add 1 or -1 depending on the sign of something (you can see that these operators just treat True as 1 and False as -1):

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

We could even enclose the declarations in a class called Signed, for easier use, including proper signatures and fixity.

This way, the above general examples would simplify, not just in code, but also in execution time and space, because we avoid multiplication (using (*.) instead), we avoid extra comparison once we have a Bool, we can get the sign from one type of data and use it for another type without needing type conversion, and we use the short type Bool instead of a potentially long type of class Num. But we get more flexibility, while allowing some optimization of code and data types.

My question, then, is if there are cases different than the three general use cases exposed here, that is, cases not easily covered by this approach, cases for which the current signum function is advantageous against a Bool sign approach. More precisely, can I avoid completely the use of the current signum function without losing efficiency or code clarity?


Edit: I modified the first paragraph to a more "neutral" fashion, following Reid Barton comment.


Progress update: the code for this approach was greatly improved for simplicity and clarity, with the great help of the current answers and comments.

Upvotes: 2

Views: 2087

Answers (3)

David Young
David Young

Reputation: 10793

To address the question of time complexity:

Branches are not free and if you have to (in concept) multiply values by the result of signum of the same value in several different spots, it would likely be more efficient to have let s = signum x in ... or have that binding in a where-clause. You no longer have to go through a branch every time. Also keep in mind that in some cases, code can slow down due to branching behavior that goes against what the branch predictor expects.

For example, say you have code like this:

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

Analysis of efficiency is often not as clear cut as you might expect and can be highly dependent on very specific aspects of a particular program, as can be seen in the question I linked above, hence the oft quoted advice of "profile before optimizing". In the question I linked to, the version of the code which executes more instructions actually runs faster than the version that executes fewer instructions (I can verify these results on my machine, even if I include the extra instructions of sorting in the profiling)!

Upvotes: 4

Reid Barton
Reid Barton

Reputation: 15029

I've used a function like this to navigate a cursor to a target offset in a square grid by a series of moves to (orthogonally and diagonally) adjacent cells.

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

Upvotes: 4

Daniel Wagner
Daniel Wagner

Reputation: 153172

You are assuming that "positive" and "negative" are the only two possible signs. But for e.g. Complex Double, the signum operation returns a complex number with the same "direction" but magnitude 1:

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

Upvotes: 9

Related Questions