core icon indicating copy to clipboard operation
core copied to clipboard

Non Standard behavior on division by zero

Open CSDUMMI opened this issue 5 years ago • 14 comments

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:

  1. (/) : Float -> Float -> Float
  2. (//) : Int -> Int -> Int
  3. modBy : Int -> Int -> Int
  4. 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 numbers 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

CSDUMMI avatar Dec 14 '19 13:12 CSDUMMI

https://gist.github.com/CSDUMMI/4ab5159eff143b4069493ec392a7d92c

CSDUMMI avatar Dec 14 '19 13:12 CSDUMMI

By the way GHC returns Infinity:

> 1/0
Infinity

But also doesn't allow for Infinity == 1/0

CSDUMMI avatar Dec 14 '19 14:12 CSDUMMI

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 avatar Dec 15 '19 23:12 JoshuaHall

@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.

CSDUMMI avatar Dec 16 '19 18:12 CSDUMMI

Thus, I will create a pull request, to add a note for this behavior to the docs.

CSDUMMI avatar Dec 16 '19 18:12 CSDUMMI

If anybody knows of any further functions implementing some kind of division in the core library, please write a comment.

CSDUMMI avatar Dec 16 '19 19:12 CSDUMMI

@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.

JoshuaHall avatar Dec 17 '19 15:12 JoshuaHall

Just wanted to clarify my point.

CSDUMMI avatar Dec 17 '19 15:12 CSDUMMI

I wanted to say, that the current state isn't viable.

CSDUMMI avatar Dec 18 '19 09:12 CSDUMMI

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

image

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.

CSDUMMI avatar Dec 20 '19 13:12 CSDUMMI

Is this still a bug?

CSDUMMI avatar Jun 30 '20 15:06 CSDUMMI

@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:

  1. modBy crashing (it should just return 0 in my opinion, for the same reason you return 0 for 1 // 0);
  2. remainderBy returning NaN : Int, which is a type system hole (again, it should just return 0).

In any case, this should be closed as a duplicate of #590

miniBill avatar Nov 02 '20 19:11 miniBill

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.

chrisdone avatar Dec 21 '21 11:12 chrisdone

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.

CSDUMMI avatar Dec 21 '21 21:12 CSDUMMI