New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistencies with various division operators and NaN/Infinity #590

Closed
jvoigtlaender opened this Issue May 10, 2016 · 15 comments

Comments

Projects
None yet
8 participants
@jvoigtlaender
Contributor

jvoigtlaender commented May 10, 2016

Here is a collection of facts about current arithmetics behavior in Elm:

  1. 1 / 0 results in Infinity (type Float)
  2. 1 // 0 results in 0 (type Int)
  3. 1 % 0 throws a runtime error (type Int)
  4. 1rem0 results in NaN (type Int)
  5. functions isNaN and isInfinity both have type Float -> Bool

Here's several oddities/inconsistencies about those:

  • Saying that 1 divided by 0 is 0 is very strange. (2.)
  • Throwing a runtime error for one form of "mod division by 0 on integers", namely %, but returning a result for another form of "mod division by 0 on integers", namely rem, is inconsistent. (3. vs. 4.)
  • Having an expression that results in NaN of type Int, but not having the possibility of checking for NaN on type Int, seems broken design. (4. and 5.)

I propose to change the definitions of //, %, isNaN and isInfinity such that:

  1. 1 / 0 results in Infinity (type Float, as before)
  2. 1 // 0 results in Infinity (type Int, adapted behavior)
  3. 1 % 0 results in NaN (type Int, adapted behavior)
  4. 1rem0 results in NaN (type Int, as before)
  5. functions isNaN and isInfinity both have type number -> Bool (generalized types, to be applicable to both Float and Int)

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.

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

mgold May 10, 2016

Contributor

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.

Contributor

mgold commented May 10, 2016

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.

@jvoigtlaender

This comment has been minimized.

Show comment
Hide comment
@jvoigtlaender

jvoigtlaender May 10, 2016

Contributor

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

Contributor

jvoigtlaender commented May 10, 2016

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

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

mgold May 10, 2016

Contributor

2, 3, and 4 should either all return 0 or all throw runtime errors. 1 and 5 stay unchanged.

Contributor

mgold commented May 10, 2016

2, 3, and 4 should either all return 0 or all throw runtime errors. 1 and 5 stay unchanged.

@jvoigtlaender

This comment has been minimized.

Show comment
Hide comment
@jvoigtlaender

jvoigtlaender May 10, 2016

Contributor

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.

Contributor

jvoigtlaender commented May 10, 2016

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.

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

mgold May 10, 2016

Contributor

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.

Contributor

mgold commented May 10, 2016

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.

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

rtfeldman May 16, 2016

Member

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.

Member

rtfeldman commented May 16, 2016

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.

@rtoal

This comment has been minimized.

Show comment
Hide comment
@rtoal

rtoal Jul 20, 2016

I just ran into case 2.

Anyone else seen a language in which 5 // 0 was 0? I found that really weird.

rtoal commented Jul 20, 2016

I just ran into case 2.

Anyone else seen a language in which 5 // 0 was 0? I found that really weird.

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

mgold Jul 21, 2016

Contributor

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.

Contributor

mgold commented Jul 21, 2016

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.

@evancz

This comment has been minimized.

Show comment
Hide comment
@evancz

evancz Sep 22, 2016

Member

Consolidated all the math related stuff into the #721 meta issue. Follow along there!

Member

evancz commented Sep 22, 2016

Consolidated all the math related stuff into the #721 meta issue. Follow along there!

@ohanhi

This comment has been minimized.

Show comment
Hide comment
@ohanhi

ohanhi May 4, 2017

I just noticed you can actually coerce Infinity to be an Int:

> round (1 / 0)
Infinity : Int

This does not seem intentional, either.

ohanhi commented May 4, 2017

I just noticed you can actually coerce Infinity to be an Int:

> round (1 / 0)
Infinity : Int

This does not seem intentional, either.

@fHachenberg

This comment has been minimized.

Show comment
Hide comment
@fHachenberg

fHachenberg Jul 17, 2017

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.

fHachenberg commented Jul 17, 2017

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.

@jvoigtlaender

This comment has been minimized.

Show comment
Hide comment
@jvoigtlaender

jvoigtlaender Jul 17, 2017

Contributor

For the record:

  • 1 / 0 returning Infinity is what Elm currently does, not something that goes back to a proposal of mine
  • sqrt(-1) returning NaN isn't part of anything I proposed either
Contributor

jvoigtlaender commented Jul 17, 2017

For the record:

  • 1 / 0 returning Infinity is what Elm currently does, not something that goes back to a proposal of mine
  • sqrt(-1) returning NaN isn't part of anything I proposed either
@fHachenberg

This comment has been minimized.

Show comment
Hide comment
@fHachenberg

fHachenberg Jul 17, 2017

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.

fHachenberg commented Jul 17, 2017

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

This comment has been minimized.

Show comment
Hide comment
@jvoigtlaender

jvoigtlaender Jul 17, 2017

Contributor

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

Contributor

jvoigtlaender commented Jul 17, 2017

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

@rtavenner

This comment has been minimized.

Show comment
Hide comment
@rtavenner

rtavenner Dec 29, 2017

I suggest defining m % 0 and rem m 0 to return m. Here is my reasoning:

  • I would expect m and m % n to be congruent mod n, whenever m % n is defined. There's a reason it's called the modulo operator!
  • Elm has a policy of "No Runtime Exceptions", so m % n should always be defined.
  • So for all m and n, m % n exists, and is congruent to m mod n.
  • By the definition of congruence mod n, m - (m % n) is a multiple of n.
  • Setting n equal to zero, m - (m % 0) is a multiple of zero.
  • Therefore, m - (m % 0) = 0, and so m % 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.

rtavenner commented Dec 29, 2017

I suggest defining m % 0 and rem m 0 to return m. Here is my reasoning:

  • I would expect m and m % n to be congruent mod n, whenever m % n is defined. There's a reason it's called the modulo operator!
  • Elm has a policy of "No Runtime Exceptions", so m % n should always be defined.
  • So for all m and n, m % n exists, and is congruent to m mod n.
  • By the definition of congruence mod n, m - (m % n) is a multiple of n.
  • Setting n equal to zero, m - (m % 0) is a multiple of zero.
  • Therefore, m - (m % 0) = 0, and so m % 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment