Round, ceil, floor on numbers and Numeric, also: BigDecimal#round

2,836 views
Skip to first unread message

Simon Ochsenreither

unread,
Jan 31, 2014, 6:34:05 PM1/31/14
to scala-i...@googlegroups.com
Hi,

I have been working on a stopgap fix for 123456789.round, and this escalated a bit (positively) into updating the Numeric typeclass with those numbers and improving the BigDecimal API around round a bit.

Could you have a look at https://github.com/scala/scala/pull/3435?

I recommend viewing the second commit with whitespace changes removed: https://github.com/soc/scala/commit/4b250e91a6e7950ec851fb72c57637c52616cc3a?w=1

Opinions?

Bye,

Simon

Rex Kerr

unread,
Jan 31, 2014, 7:02:10 PM1/31/14
to scala-i...@googlegroups.com
In particular, the issue is that Java's BigDecimal defines a round method that takes a MathContext argument that rounds to a certain number of decimal places.  That was copied in Scala's BigDecimal, but since some operations (e.g. addition) do not apply rounding by default, there's a need to be able to invoke the included MathContext.  To distinguish this from rounding to an integer, I called the new method rounded.

The question is whether to allow BigDecimal to follow the convention of primitive types where "round" without arguments rounds to the nearest integer.

(Further question: does it return a BigInt, or a BigDecimal?  Arguably, to be consistent with java.lang.Math.rint, BigDecimal.rint should return a BigDecimal, while BigDecimal.round should just be an alias to .toBigInt.)

Personally, I am uncertain whether it is a good idea to try to harmonize the postfix operations on primitives with the methods supplied on BigDecimal.

  --Rex


--
You received this message because you are subscribed to the Google Groups "scala-internals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to scala-interna...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

Simon Ochsenreither

unread,
Jan 31, 2014, 7:08:22 PM1/31/14
to scala-i...@googlegroups.com

(Further question: does it return a BigInt, or a BigDecimal?  Arguably, to be consistent with java.lang.Math.rint, BigDecimal.rint should return a BigDecimal, while BigDecimal.round should just be an alias to .toBigInt.)

I think it should return the type itself.¹ If one wants to have a BigInt, one can just call toBigInt.

¹ I think it's not possible to do it anoter way anyway, because those operations are either  T => T or T => <some fixed type>. Picking number 1 for some types and number 2 for some others is not possible.

Rex Kerr

unread,
Jan 31, 2014, 7:14:00 PM1/31/14
to scala-i...@googlegroups.com
scala> 2.7.round
res0: Long = 3

scala> 2.7f.round
res1: Int = 3

What should BigDecimal("2.7").round return?

  --Rex



--

Erik Osheim

unread,
Jan 31, 2014, 7:30:29 PM1/31/14
to scala-i...@googlegroups.com
On Fri, Jan 31, 2014 at 04:14:00PM -0800, Rex Kerr wrote:
> scala> 2.7.round
> res0: Long = 3
>
> scala> 2.7f.round
> res1: Int = 3
>
> What should BigDecimal("2.7").round return?

So, I would say that BigDecimal should have a round method, and that
it should return a BigDecimal. But I understand the consistency
arguments.

It seems obvious to me that RichDouble#round should return a Double
and RichFloat#round should return a Float. This is one of those things
that Spire already fixes, but due to Predef and Rich* types, there is
a limit to how much we can do unless the user explicitly disables the
Rich conversions, or uses generics.

import spire.implicits._
import spire.math._
def foo[A: Fractional](a: A): A = a.round

1e20.round
Long = 9223372036854775807

foo(1e20)
Double = 1.0E20

Anyway, just my 2c. I've attached the implementation(s) we're using
for Double and Float.

-- Erik

final def round(a: Float): Float =
if (Math.abs(a) >= 16777216.0F) a else Math.round(a).toFloat

final def round(a: Double): Double =
if (Math.abs(a) >= 4503599627370496.0) a else Math.round(a).toDouble

Simon Ochsenreither

unread,
Jan 31, 2014, 7:34:49 PM1/31/14
to scala-i...@googlegroups.com, er...@plastic-idolatry.com

So, I would say that BigDecimal should have a round method, and that
it should return a BigDecimal. But I understand the consistency
arguments.

+1
 
It seems obvious to me that RichDouble#round should return a Double
and RichFloat#round should return a Float.

+1
 
This is one of those things
that Spire already fixes, but due to Predef and Rich* types, there is
a limit to how much we can do unless the user explicitly disables the
Rich conversions, or uses generics.

I'm wondering whether we can fix this with a @bridge annotation in a compatible way.

Rex Kerr

unread,
Jan 31, 2014, 7:42:09 PM1/31/14
to scala-i...@googlegroups.com
On Fri, Jan 31, 2014 at 4:30 PM, Erik Osheim <er...@plastic-idolatry.com> wrote:
On Fri, Jan 31, 2014 at 04:14:00PM -0800, Rex Kerr wrote:
> scala> 2.7.round
> res0: Long = 3
>
> scala> 2.7f.round
> res1: Int = 3
>
> What should BigDecimal("2.7").round return?

So, I would say that BigDecimal should have a round method, and that
it should return a BigDecimal. But I understand the consistency
arguments.

It seems obvious to me that RichDouble#round should return a Double
and RichFloat#round should return a Float. This is one of those things
that Spire already fixes, but due to Predef and Rich* types, there is
a limit to how much we can do unless the user explicitly disables the
Rich conversions, or uses generics.

Er, this is what rint is for.  "round" is the method that converts a floating-point type to the closest matching integer type.  "rint" is the method that converts a floating-point type to the closest integer representation.

Consider code like this:

  def rgb(r: Float, g: Float, b: Float) = ((r*255).round, (g*255).round, (b*255).round)

All this will break if you change round.

It might not have been the best choice, but it's way too late to change one's mind now.  (Absent the mythical Scala 3.0 code rewriting tool.)

  --Rex
 

Simon Ochsenreither

unread,
Jan 31, 2014, 7:42:37 PM1/31/14
to scala-i...@googlegroups.com, er...@plastic-idolatry.com
This is one of those things
that Spire already fixes, but due to Predef and Rich* types, there is
a limit to how much we can do unless the user explicitly disables the
Rich conversions, or uses generics.

I'm wondering whether we can fix this with a @bridge annotation in a compatible way.

Seems to work:

package scala
import annotation.bridge
object Foo {
  @bridge
  def round: Int = round.toInt
  def round: Float = 1.0f
}

javap scala.Foo
Compiled from "bridge.scala"
public final class scala.Foo {
  public static float round();
}


Shall we fix this?

Erik Osheim

unread,
Jan 31, 2014, 7:46:22 PM1/31/14
to scala-i...@googlegroups.com
On Fri, Jan 31, 2014 at 04:42:09PM -0800, Rex Kerr wrote:
> It might not have been the best choice, but it's way too late to change
> one's mind now. (Absent the mythical Scala 3.0 code rewriting tool.)

I am not sure I agree that it's "way too late", but absent a larger
plan I will defer to you on this. This is why you are helping to
manage Scala's standard library and I am off in my own universe. :)

Thanks for the tip about rint. Somehow I had missed that, I'll
definitely start using it for Double.

-- Erik

Simon Ochsenreither

unread,
Jan 31, 2014, 8:29:13 PM1/31/14
to scala-i...@googlegroups.com

"rint" is the method that converts a floating-point type to the closest integer representation.

What's the difference to toX, except the unreadable name?

Rex Kerr

unread,
Jan 31, 2014, 8:32:53 PM1/31/14
to scala-i...@googlegroups.com
rint rounds a Double to the closest Double representation to the whole number closest to the original double.  That is, it does the rounding and keeps the type.

Is it weird that round changes types and rint (which has the name of a new type in the method name!!) doesn't?  Yes.

But that's the convention, and people rely on it who do this sort of thing.  (Well, they do if they know the methods exist at all.  They're likely to just do an import and then round(x), rint(y).)

  --Rex


On Fri, Jan 31, 2014 at 5:29 PM, Simon Ochsenreither <simon.och...@gmail.com> wrote:

"rint" is the method that converts a floating-point type to the closest integer representation.

What's the difference to toX, except the unreadable name?

--

Simon Ochsenreither

unread,
Jan 31, 2014, 8:56:08 PM1/31/14
to scala-i...@googlegroups.com
"It's a convention somewhere" is in large parts responsible for the messy parts of Scala.

I prefer consistency.

Rex Kerr

unread,
Jan 31, 2014, 9:05:02 PM1/31/14
to scala-i...@googlegroups.com
It's already completely consistent, save for BigDouble using the name "round".  This isn't about consistency, it's bike-shedding (i.e. preferring one name over another).

  --Rex



On Fri, Jan 31, 2014 at 5:56 PM, Simon Ochsenreither <simon.och...@gmail.com> wrote:
"It's a convention somewhere" is in large parts responsible for the messy parts of Scala.

I prefer consistency.

--

Tobias Roeser

unread,
Feb 1, 2014, 5:03:26 AM2/1/14
to scala-i...@googlegroups.com
Isn't @bridge deprecated since 2.10?

Where can be more information found about what a "bridge method" is and in
what way this annotation affects the generated byte code?

Thanks,
Tobias

Simon Ochsenreither

unread,
Feb 1, 2014, 4:59:54 PM2/1/14
to scala-i...@googlegroups.com, er...@plastic-idolatry.com

er...@plastic-idolatry.com

unread,
Feb 1, 2014, 6:19:42 PM2/1/14
to scala-i...@googlegroups.com
On Sat, Feb 01, 2014 at 01:59:54PM -0800, Simon Ochsenreither wrote:
> what do you think about:
> https://github.com/soc/scala/commit/83e0b9a73bdc294feedb7d965b78f39a6816bede

If you agree with Rex (that round being A => A would be a dealbreaker
due to divergence with Java) then obviously it's a problem. I respect
this position, and it has certainly driven many other library
decisions in Scala.

Personally I think this change would be a net win for the language in
the long term. But like I said before it's not my job to worry about
these kinds of trade-offs; I prefer to continuously move toward
"better" even if it causes some temporary confusion [1].

-- Erik

[1] I have seen Scala developers using round() without realizing it
has only a limited range where it is "safe" (and otherwise it produces
a drastically wrong answer). I imagine this issue hurts Java
developers less since they must annotate more of their types.

So the balancing act is between confusing Java programmers and
confusing Scala programmers IMO. And since Double => Long is not a
valid promotion, I doubt any programmer hoping to use round() to get a
Long will be confused for long upon seeing the type error.

Rex Kerr

unread,
Feb 1, 2014, 7:35:40 PM2/1/14
to scala-i...@googlegroups.com, Erik Osheim
It's also important to understand the scale of problems this will cause for numerically intensive code.  For people doing numerical work using what the library provides, a type change is a huge breaking change.  And you can't have inconsistency between x.round and round(x) without far worse confusion than merely returning an unexpected type.  A quick grep shows 714 lines in my main development directory with "round" without a corresponding "toInt" that would take care of things if the type changed.

That's a huge headache to fix without an awesome refactoring tool.

(For comparison, List, Map, and Set together take only 560 lines.)

  --Rex



--

Simon Ochsenreither

unread,
Feb 1, 2014, 8:37:00 PM2/1/14
to scala-i...@googlegroups.com, Erik Osheim

It's also important to understand the scale of problems this will cause for numerically intensive code.

It will probably run more correctly afterwards?
 
And you can't have inconsistency between x.round and round(x) without far worse confusion than merely returning an unexpected type.

What inconsistency?
 
A quick grep shows 714 lines in my main development directory with "round" without a corresponding "toInt" that would take care of things if the type changed. That's a huge headache to fix without an awesome refactoring tool.

Use an regex, an IDE?

Rex Kerr

unread,
Feb 1, 2014, 8:49:35 PM2/1/14
to scala-i...@googlegroups.com, Erik Osheim
On Sat, Feb 1, 2014 at 5:37 PM, Simon Ochsenreither <simon.och...@gmail.com> wrote:

It's also important to understand the scale of problems this will cause for numerically intensive code.

It will probably run more correctly afterwards?

Very little chance of that if the code's been written by someone who works with numeric code a lot.  (Presumably why they have so much of it.)  Having to change something complicated that you have previously tested generally increases the chance of failure.

For people writing new code it may increase the chance of correctness; depends whether they need to keep the old behavior in mind also.  If yes, having to keep more in mind generally would increase error rates.  If no, it's likely a win.
 
 
And you can't have inconsistency between x.round and round(x) without far worse confusion than merely returning an unexpected type.

What inconsistency?

import java.lang.Math._
round(x)

import scala.math._
round(x)

x.round

Will these all agree?  Will you ever have to write code where you're restricted to one?

For people maintaining mixed codebases, having to make a mental switch between behaviors just adds to the burden.  There is a lot of numeric code written in Java that has no equivalently fast/featureful Scala equivalent.  (Apache Commons Math, for instance.)

 
A quick grep shows 714 lines in my main development directory with "round" without a corresponding "toInt" that would take care of things if the type changed. That's a huge headache to fix without an awesome refactoring tool.

Use an regex, an IDE?

Regex is good when you don't also use the word "round" to mean a shape.  Alas for people who do math on shapes.  Is IDE refactoring good enough to selectively get the .rounds that are on Double or Float, and the round()s that are from scala.math?

  --Rex
 

Simon Ochsenreither

unread,
Feb 1, 2014, 9:41:29 PM2/1/14
to scala-i...@googlegroups.com, Erik Osheim

import java.lang.Math._
round(x)

import scala.math._
round(x)

x.round

Will these all agree?  Will you ever have to write code where you're restricted to one?

The Scala ones will agree. Regarding Java: all hope is lost there anyway. We differ from it in many parts already, and I don't have ever seen people complain about it. For instance:

scala> java.lang.Double.MIN_VALUE
res0: Double = 4.9E-324

scala> Double.MinValue
res1: Double = -1.7976931348623157E308


Additionally, while C# copied tons of stuff from Java, they also fixed this: http://msdn.microsoft.com/en-us/library/system.math.round%28v=vs.110%29.aspx

Regex is good when you don't also use the word "round" to mean a shape.  Alas for people who do math on shapes.  Is IDE refactoring good enough to selectively get the .rounds that are on Double or Float, and the round()s that are from scala.math?

You could just introduce the round method locally, rename it and delete the method again.
For 2.12, I'm planning to look into a way of providing better migration support for the IDE. (E. g. by having an annotation which describes the refactoring at the definition site of the code.)
Reply all
Reply to author
Forward
0 new messages