core
core copied to clipboard
Non Standard behavior on division by zero
There is no answer to the equation x = a/0
where a ∈ R.
Not even x = +∞
or x = - ∞
are valid.
So programming languages have to have a way of dealing with this
mathematical undefined.
Some languages crash (Python 3, Perl 5) others return Infinity
(JS, Haskell)
and Elm does all of it.
There are 4 functions in the Basics Module in elm-core, that use division in some kind:
-
(/) : Float -> Float -> Float
-
(//) : Int -> Int -> Int
-
modBy : Int -> Int -> Int
-
remainderBy : Int -> Int -> Int
There are others, but those that I found used these functions under the hood (degrees for example). But how do they behave?
> 1/0
Infinity : Float
> 1//0
0 : Int
> modBy 0 5
Error: Cannot perform mod 0. Division by zero error.
> remainderBy 0 5
NaN : Int
What first caught my eye about this was that modBy 0 5
threw a runtime error,
something Elm isn't supposed to rely upon as anything but last resort.
But more important for me was, that none of these functions returned the same
value, when they had to say "I can't compute this".
Even more problematic is that this behavior isn't explained in the documentation.
And because most of these values are still number
s and some (0) even reasonable
numbers, programmers mightn't even notice that they are from now on using an "undefined"
value to compute real results.
Because in Elm 1//0 == 0//1 == True
, users of (//)
will not have a way
of identify wheter they just calculated a zero division, without keeping the divisor.
And for those who use (/)
or remainderBy
, they'll have to use >
and <
to identify Infinity
and NaN
, because the Elm compiler doesn't compile
a program using these values as functions.
I don't know what to do about this,
whether there should be a value (like NaN in JS) that
is part of the number
class and can be matched for with ==
or case .. of
statements, that signifies "I can't compute this".
Or that all these functions return a Maybe, where Nothing == "I can't compute this"?
What I do know is that this behavior should be known to the programmer and thus
added in a note to the documentation of the functions in question.
I have written an Elm Module, that I will attach, with all the examples I gave in a separate function. It compiles!
By the way, there have been many issues about this and I will reference some them here: #909 #1034 #565 #590 #932
https://gist.github.com/CSDUMMI/4ab5159eff143b4069493ec392a7d92c
By the way GHC returns Infinity:
> 1/0
Infinity
But also doesn't allow for Infinity == 1/0
The modBy
example is especially egregious considering it compiles on the latest Elm version and on the Elm homepage it says that Elm has "No Runtime Exceptions".
@JoshuaHall
That certainly isn't a viable solution.
But my point is more that the inconsistency in how elm signals "I can't compute this" is misleading, not documented and thus often it is impossible to prevent it.
( If you don't check the number beforehand, which hasn't been the way Elm deals with possible errors, as far as I can see it.)
Because neither NaN
nor Infinity
as Int
are compiled.
And 0 can also be a valid result for any calculation of the form 0/x
where x ∈ R \ {0}.
This can be a problem, once you use any of these operations (without further research) in a vital computation. There modBy
may even be the most gracious implementation, because it doesn't continue the computation with impossible values.
Anyway, I don't think that this behavior can stay the way it is or alter into a different undocumented and inconsistent way, because it makes for “error-prone” languages.
Thus, I will create a pull request, to add a note for this behavior to the docs.
If anybody knows of any further functions implementing some kind of division in the core library, please write a comment.
@CSDUMMI I didn't propose a solution, all I meant was that what is an operator in many other languages probably shouldn't throw a runtime error. I do not mean that Elm should never throw a runtime error in any circumstance, since in certain situations there is no elegant way to recover from an error, of course, but this is not one of them.
Just wanted to clarify my point.
I wanted to say, that the current state isn't viable.
Proposal
After thinking this issue through a bit, I have come up with two possible solutions, one which is a breaking change for many projects and another, that can be used before introducing a breaking change. I do this, because I have heard Elm isn't afraid of breaking changes and thus propose something for the short and long term.
The Solutions
I don't want to further the misunderstanding, that division by 0 is Infinity. It is true, that
But that isn't what you try to compute with division by 0.
Thus I won't propose to return Infinity
.
I think that the most secure way and the way Elm deals with errors in general, is the right way to go here as well.
Using Maybe a
, we can return Nothing
, in case of division is by 0 and otherwise Just x
, where x is the result of the division.
But because this will be a breaking change,
I'd propose a temporary solution by introducing the value Uncomputable
as part of Int
and Float
, which is returned on division by 0.
Code using the two solutions
The most important thing is, to see how code would look like under the two implementations.
Let's say you want to calculate the median value of some List Float
:
Using Uncomputable
:
median : List Float -> Maybe Float
median xs = case (List.sum xs)/(List.length xs) of
Uncomputable -> Nothing
x -> x
The important bit to note, is that it is at the choice of the developer, if they want handle the error or not, they could have just as well written:
median : List Float -> Float
median xs = (List.sum xs) / (List.length xs)
The users of that function could then match for Uncomputable
, if they wanted.
And if it ever was displayed or used in another calculation, Uncomputable
would be displayed / returned.
This temporary solution has the benefit of misleading nobody, being consistent
and making it possible for those who want to check for such an error, to do so.
And it, if Maybe a
is eventually used as the result type of these functions,
many programmers would already have the right structure in their programs to handle it, because the jump from the previous example to this:
median : List Float -> Maybe Float
- median xs = case (List.sum xs) / (List.length xs) of
- Uncomputable -> Nothing
- x -> x
+ median xs = (List.sum xs) / (List.length xs)
{-| In usage nothing changes -}
case median some_list of
Nothing -> Error Handeling
Just x -> x
is small and some of the programs will have adapted already.
Of course, Elm could skip this step as well and just introduce Maybe a
,
which might be much more comprehensible for many.
Is this still a bug?
@CSDUMMI Yes it is.
Returning Maybe
from a division, while technically correct is very much NOT hergonomic, and personally I favor the current 1//0 = 0
behavior, which I don't consider a bug.
If you want to read more, this is a nice article on dividing by zero.
1 / 0 = Infinity
otoh is just standard Float
behavior, definitely not a bug.
What I consider bugs are:
-
modBy
crashing (it should just return0
in my opinion, for the same reason you return0
for1 // 0
); -
remainderBy
returningNaN : Int
, which is a type system hole (again, it should just return0
).
In any case, this should be closed as a duplicate of #590
The problem with producing any number at all for n/0 is that the result can march on in full confidence through your call graph until the cows come home, perhaps not being detected for a while.
If you just threw an error loudly immediately, the programmer would have some information where the error happens.
Returning a Maybe doesn't help because it's hard to imagine any possible scenario where you would "try" to do some arithmetic and when it fails that you could do something else useful in the program other than terminate.
You've got a point there. I'm most troubled by the way that different division and mod functions handle division by zero.
If they were all adhering to a documented standard this'd be no longer an issue.