Is the modulo operation signature in sync with the mathematical definition?

Why does the modulo operation result take the dividend’s type instead of the divisor’s type?

This fact is not affecting the daily work of Scala users, but it looks like it is not in sync with the mathematical law following the modulo operation.

I am not a mathematician and I don’t know in deep the internal implementation of this operation, so pretty sure that there is an explanation behind it.

As I asked in Stackoverflow days ago, why Scala use these signatures for the module?

For example, in scala.Long

  def %(x: Byte): Long
  def %(x: Short): Long
  def %(x: Char): Long
  def %(x: Int): Long
  def %(x: Long): Long
  def %(x: Float): Float
  def %(x: Double): Double

I think that the response type should be always the same type of x, because the remainder after the division always is going to fit into the divisor type.

So why the response type is taken from the dividend?

Should be next one the right signature for scala.Long?

  def %(x: Byte): Byte
  def %(x: Short): Short
  def %(x: Char): ???
  def %(x: Int): Int
  def %(x: Long): Long
  def %(x: Float): Float
  def %(x: Double): Double

It is curious that when x is Double or Float, the signature is right.

In Stackoverflow, one response is pointing to Java compatibility. So this response generates another question: Is it necessary to keep this compatibility even if the definition does not match with the mathematical concept that it is implementing?

I don’t think that any mathematical definition is being violated. The result types could be narrower but they are not necessarily incorrect. For starters mathematics doesn’t know the concepts of ints or longs or shorts or bytes.

Scala could probably implement % to return the smallest possible type, but then % would probably become slower. Java bytecode only has instructions for taking the remainder of 2 ints, 2 longs, 2 doubles or 2 floats. So then in 1003L % (4: Byte) Scala should have to insert an extra type conversion after the remainder instruction. While in many cases the programmer is using it in some low level performance sensitive code, but doesn’t care that much whether a Long or a Byte comes out of there.

3 Likes

Yes, you are right in all your points. Mainly:

  • It is not violating the mathematical definition.
  • Maths does not know about Long or Int or any other programming language type.

But it is true as well that the remainder, by definition (remainder part is going to be lower than the divisor part), always is going to in the divisor type.

The issue that I had was that I did not cast the result of the modulo to Int, so I was serializing and storing it as Long. In this way, the size in memory (and in the disk as well) was the double I was expecting.

So as a takeaway:

Scala is returning the same type that Java to avoid the performance penalty of casting.

Am I right?

I’m not a JIT expert and perhaps there is no penalty in practice. Maybe changing the return types from what Java does simply wasn’t ever considered. But at least the fact that the modulo operation you are suggesting is not natively supported by the bytecode should be taken into consideration for the primitive operations that a language exposes.

2 Likes

This sounds like a pretty sketchy interpretation of math or types.

There is a strong expectation that a basic mathematical binary operator between a Long and an Int will return Long.

If you violate that expectation, people will write expressions that they assume to be Long, but they are actually Int, and that will lead to bugs.

FWIW, I would be fine with making mixed-type numerical expressions illegal (at least by default, maybe with opt-out), because they lead to countless bugs and it is not clear why one would want to mix numerical types.

3 Likes

By “mixed-type” do you mean in the parameters (e.g. (Int, Long) => Long), or in the return type (e.g. (Int, Int) => Long) ?

1 Like

All those operators and functions that are mathematically understood to take one or more elements from some set and return a value from the same set, should have all arguments and the return value of the same type.

That includes plus, minus, divide by, times, modulo, abs, min, max, sin, cos, tan, exp, log, acos, cosh, etc.

One could argue about pow, but I see there only seems to be a pure Double version anyway, which is fine.

Obviously, round should still take Float/Double and return Int/Long. Also, bit shift ops may be mixed.

2 Likes

How do you feel about operations widening their output type to minimize overflow errors and help mitigate bugs like the one famously found in reference binary search implementations?

All those operators and functions that are mathematically understood to take one or more elements from some set and return a value from the same set, should have all arguments and the return value of the same type.

Don’t quite understand, did you suggest that if there is a common set habited by all parameter values of a function then it should also be the output set?

E.g.: fun:(Int,Long)=>Long because the common set is: Long:>Int

You mean like specialPlus(Int, Int):Long? Why not convert to Long before you add them?

2 Likes

3.14 % 3

In essence, yes.

pow is probably a pretty good candidate for this, as the return values grow quickly, abs is a horrible candidate for this as the return values don’t grow at all.

I guess the core of my question is what are your thoughts on where the tipping point lies between these two extremes?

Multiplication can be made to overflow fairly easily, but we’ve even run into this with addition, so where should the line be drawn between “mixed types are illegal” and “specialized return types can mitigate footguns”? Should this depend on the types (e.g. it’s much easier to overflow a Byte than a Long) ?

I’m guessing that you mean that if 3.14 % 3 follows the divisor type, it’ll run into issues because 3.14 % 3.0 is 0.14, which isn’t representable using an Int. Can you confirm?

I meant mathematical sets like real numbers or integers.

I.e. when a function in math takes real numbers and gives a real number, then the computational representation of such a number should use a consistent type to represent real numbers. Same for integers.

Basically, don’t mix Float and Double, and don’t mix Int and Long.

1 Like

Indeed. 3.14 % 3 is 0.14000000000000012 (on my machine).
If we accept that this makes sense, then the result of modulo operator needs to be of the same type as the dividend, and not the divisor. Meaning that the existing implementation works just fine.

1 Like

I’m not entirely sure what you are proposing. My concern is primarily about standard math stuff like, for example, trigonometric functions like tangent.

In scala.math, there are two functions for tangent, tan(Float):Float and tan(Double):Double. Tangent diverges even for small values like pi/2. But you can’t just add tan(Float):Double, because you can’t overload functions by return type only, so we’d need a different name. Perhaps tanBigResult(Float):Double?

But the function for tangent has been called tan since my childhood. I’m not sure others will understand me if I use a different name.

Besides, how is tanBigResult implemented? Probably like this:


def tanBigResult(x: Float): Double = tan(x.toDouble)

Is the new method really better than just writing tan(x.toDouble) myself?

Also, now that we have invested in creating a double precision value to hold the result, it is really a pity that we do not actually have double precision, because we derived it from a single precision value. That’s wasteful. That’s why we want the input values also to be double precision. Just use Double throughout.

1 Like

Sorry for the confusion, not really proposing anything. I’m mostly trying to understand the domain a bit better via hypotheticals.

Supposing we decide that some method has problematic overflow/divergence behavior, the criteria for this and what we should do about it is primarily what I’m curious about.

I’ll use tan for continuity and I don’t want to imply I think it’s particularly problematic.

For tan, we currently have tan(Float):Float and tan(Double):Double, and a possible way widening the output to mitigate divergence would be changing the definition of tan(Float):Float to tan(Float):Double. I don’t have enough domain knowledge to suggest if tan(Double):Double might be better as tan(Double):BigDecimal, but the pattern could hold if that’s determined to be a useful thing.

Making the types uniform would definitely simplify things, and that’s very appealing. What I don’t know is if the understanding of the limitations of the various numeric types are well-enough understood to make this the right choice.

In this respect, tan is actually a perfect example: given the gaps in the way Float and Double represent numbers, are they even the right choice for a particular use case?

Maths does not know about Long or Int or any other programming language type.

Sorry, no my words. This is what I get from the @Jasper-M response. I don’t have enough maths base to argue.

Hi @linasm. Yes, as I describe in the first post, for in the case of Double and Float, the type is the same as the divisor.

  def %(x: Float): Float
  def %(x: Double): Double

My question is why is different in nondecimal cases (Long, Int, etc.)