Inconsistencies with various division operators and NaN/Infinity
jvoigtlaender opened this issue · 15 comments
Here is a collection of facts about current arithmetics behavior in Elm:
1 / 0results inInfinity(typeFloat)1 // 0results in0(typeInt)1 % 0throws a runtime error (typeInt)1rem0results inNaN(typeInt)- functions
isNaNandisInfinityboth have typeFloat -> Bool
Here's several oddities/inconsistencies about those:
- Saying that
1divided by0is0is very strange. (2.) - Throwing a runtime error for one form of "mod division by
0on integers", namely%, but returning a result for another form of "mod division by0on integers", namelyrem, is inconsistent. (3. vs. 4.) - Having an expression that results in
NaNof typeInt, but not having the possibility of checking forNaNon typeInt, seems broken design. (4. and 5.)
I propose to change the definitions of //, %, isNaN and isInfinity such that:
1 / 0results inInfinity(typeFloat, as before)1 // 0results inInfinity(typeInt, adapted behavior)1 % 0results inNaN(typeInt, adapted behavior)1rem0results inNaN(typeInt, as before)- functions
isNaNandisInfinityboth have typenumber -> Bool(generalized types, to be applicable to bothFloatandInt)
In addition to better consistency, and correcting a mathematical wrongness (1 // 0 = 0), this would have benefits in terms of efficiency, since // and % are currently performing extra checks on each invocation that would then go away.
I agree that it is consistent, but it allows ±Infinity and NaN, IEEE floating point abstractions, into the Int type, which may one day be represented as machine integers. Additionally, what should List.repeat (1//0) "foo" evaluate to? Empty list, sure, but when writing functions that take integers one loses guarantees that they are actually integers. Finally, it doesn't seem like one can perform operations on ±Infinity and NaN to obtain a finite non-integer value, but I'm still concerned about the possibility.
@mgold, do you have an alternative proposal to handle cases 1.-5. in a consistent way?
Not all of them necessarily need to be consistent. For example, 1. and 2. need not, since they are on different types. But 3. and 4. being inconsistent seems clearly undesirable, since both are some form of "mod division on integers".
2, 3, and 4 should either all return 0 or all throw runtime errors. 1 and 5 stay unchanged.
2 returning 0 seems an abomination to me. Is there any precedent (say, a programming language) in which 1 divided by 0 is given as 0?
Making all of 2, 3 and 4 throw runtime errors was exactly the content of my closed #565 and #576. @rtfeldman disagreed.
Is there any precedent for a (Turing-complete) language to try so hard to avoid runtime errors? Ask a nonsense question, get a nonsense answer. Or, crash and hopefully catch the bug in development or testing. I'm actually undecided between the two.
I'm not sure exactly what @rtfeldman was talking about when he said he wanted to remove the check, so let's see what he has to say now that we've framed the issue a little better.
To clarify, I'm not advocating one way or the other; I just wanted to note some facts (and an explanation I'd heard at some point for why some of them work the way they do) about the current state of things. 😄
I have yet to encounter any of these edge cases in practice, and don't have particularly strong feelings about this.
I just ran into case 2.
Anyone else seen a language in which 5 // 0 was 0? I found that really weird.
Is there any precedent for a (Turing-complete) language to try so hard to avoid runtime errors? Ask a nonsense question, get a nonsense answer.
To be clear, the second sentence refers to division by zero as a nonsense question, not the question that you are asking.
Consolidated all the math related stuff into the #721 meta issue. Follow along there!
I just noticed you can actually coerce Infinity to be an Int:
> round (1 / 0)
Infinity : Int
This does not seem intentional, either.
I strongly recommend to NOT make cases like (x / 0) return Infinity or sqrt(-1) return NaN as proposed above by jvoigtlaender. I come from a numerical computing background and my experience is that it's toxic to allow silent propagation of numeric exceptions. Infinity and NaN are NOT just values like any other float. In the worst case you end up with strange behaviour of your program at some distant point because a NaN result from some calculation spread through your code (NaN + x = NaN). Instead you want your program to crash as soon as possible because it obviously contains a bug.
For the record:
1 / 0returningInfinityis what Elm currently does, not something that goes back to a proposal of minesqrt(-1)returningNaNisn't part of anything I proposed either
sorry for my inprecise citation.
- I saw that you proposed "1rem0 results in NaN (type Int, as before)" and generalized to "sqrt(-1)". My argument above certainly can be generalized to "1 rem 0" (-> don't return NaN).
- Sorry that my description suggested the status quo of "1/0" returning Inifinity to be part of your proposition. I consider this status quo to be a problem.
Also 1 `rem` 0 resulting in NaN is part of the status quo, not something I thought up. 😄
But I do get that your general thrust is to raise a runtime error much more often than the status quo does. That is certainly one way to make things more consistent (than they are in the status quo, where some things raise runtime errors and others don't).
I suggest defining m % 0 and rem m 0 to return m. Here is my reasoning:
- I would expect
mandm % nto be congruent modn, wheneverm % nis defined. There's a reason it's called the modulo operator! - Elm has a policy of "No Runtime Exceptions", so
m % nshould always be defined. - So for all m and n,
m % nexists, and is congruent tommodn. - By the definition of congruence mod
n,m - (m % n)is a multiple of n. - Setting
nequal to zero,m - (m % 0)is a multiple of zero. - Therefore,
m - (m % 0) = 0, and som % 0 = m.
By the same logic, rem m 0 should equal m.
Also note that with this definition, the formula m = (m // n) * n + rem m n holds universally.