Julia: promote_type(Int16, Float16) and promote_type(Int32, Float32) produce lossy types

Created on 1 Nov 2017  Â·  12Comments  Â·  Source: JuliaLang/julia

promote_type(Int16, Float16) should produce Float32 and promote_type(Int32, Float32) should produce Float64, the smallest available types on my system which represent all of the integer values without loss. However, these two methods produce Float16 and Float32 respectively, which cannot accurately represent all of the necessary values.

This is a bug right?

   _       _ _(_)_     |  A fresh approach to technical computing
  (_)     | (_) (_)    |  Documentation: https://docs.julialang.org
   _ _   _| |_  __ _   |  Type "?help" for help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 0.6.1 (2017-10-24 22:15 UTC)
 _/ |\__'_|_|_|\__'_|  |  Official http://julialang.org/ release
|__/                   |  x86_64-apple-darwin14.5.0

help?> promote_type
search: promote_type

  promote_type(type1, type2)

  Determine a type big enough to hold values of each argument type without loss, whenever possible. In some cases, where no type exists to which both types can be promoted losslessly, some loss is tolerated; for example,
  promote_type(Int64, Float64) returns Float64 even though strictly, not all Int64 values can be represented exactly as Float64 values.

  julia> promote_type(Int64, Float64)
  Float64

  julia> promote_type(Int32, Int64)
  Int64

  julia> promote_type(Float32, BigInt)
  BigFloat

julia> promote_type(Int32, Float32)
Float32

help?> maxintfloat
search: maxintfloat

  maxintfloat(T)

  The largest integer losslessly representable by the given floating-point DataType T.

  maxintfloat(T, S)

  The largest integer losslessly representable by the given floating-point DataType T that also does not exceed the maximum integer representable by the integer DataType S.

julia> maxintfloat(Float32)
1.6777216f7

help?> typemax
search: typemax TypeMapLevel TypeMapEntry typemin TypeName

  typemax(T)

  The highest value representable by the given (real) numeric DataType.

julia> typemax(Int32)
2147483647

julia> Int32(Float32(16777217))
16777216

julia> maxintfloat(Float16)
Float16(2048.0)

julia> typemax(Int16)
32767

julia> Int16(Float16(2049))
2048

Most helpful comment

Again, all that I wish to point out in this issue is a discrepancy between what the documentation of promote_type promises and what promote_type actually does.

Most users, after reading the help entry for promote_type, would probably be pretty bamboozled when they see promote_type(Int8, UInt16) = UInt16 or promote_type(Int16, Float16) = Float32 or even (new one) promote_type(Int64, Float16) = Float16.

It looks like it is time to change the documentation for promote_type. I think saying something along the lines of "promote_type represents the default promotion behavior in Julia when mathematical operators are given arguments of differing types, and generally tries to return a type which can at least approximate most values of either type without widening the type." would be better, as long as the really counter intuitive cases are mentioned as examples to avoid bugs.

A smallest_containing_type (or widen, if you prefer) function would be very useful to preserve the old behavior of promote_type. I think several people, myself included, must have read the documentation of promote_type and assumed it was such a function.

All 12 comments

It was an intentional choice. By the same logic should Int64 and Float64 promote to Float128?

The spec for promote_type does include a clause for this, saying that the method should "Determine a type big enough to hold values of each argument type without loss, whenever possible."

So it seems that promote_type(Int64, Float64) should return Float128 only if my system has it. My system does not have Float128.

The Int128 type is hypothetical, but hopefully you see my point – once hardware catches up, we will have a Int128 type. The current behavior feels fairly coherent with other promotions we do these days. We don't, e.g. promote Int8 + UInt16 to Int32 even though Int32 can hold all the possible argument values; instead we promote to the larger of the two type – UInt16. So this may just be a matter of fixing the documentation.

If you feel the current behavior is correct, then the documentation needs to change. However, the "without loss, whenever possible." property is pretty nice.

On my computer, promote_type(Int8, UInt16) returns Int64 which can indeed hold all possible values of either of its arguments.

I can think of two easy ways to fix this problem.

  1. Change the behavior of promote_type to return one wider floating point types in the two previously mentioned cases.

  2. Change the documentation to say something like "without loss, in most cases." and then include the example

  julia> promote_type(Int32, Float32)
  Float32

promote_type(Int8, UInt16) is now UInt16: https://github.com/JuliaLang/julia/issues/9292. There was a lot of discussion of this issue and we're pretty happy with the simplicity and intuitiveness of the new rule. It would potentially make sense to have widen methods (or some other function) that can give a common type that's large enough to hold all values of two argument types.

Also Float32 * Float32 can produce values that a Float32 cannot hold and yet is not promoted to Float64 for good reasons. The promotion behaviour of promote_type(Int32, Float32) is almost secondary to this.

In general, we've found that widening of types is a thing that should be explicitly asked for when retention of precision is required. This is not what we originally believed going into this, which was much closer to one should retain precision when possible up to hardware limitations.

@mschauer The documentation of promote_type does not make any guarantees about the output being able to hold the result of binary expressions on the types given as input. promote__type makes the guarantee that it will return a type which can losslessly represent both of the types in it's input. promote_type(Float32, Float32) does not promise to produce a type which can hold any Float32 * Float32.

Again, all that I wish to point out in this issue is a discrepancy between what the documentation of promote_type promises and what promote_type actually does.

Most users, after reading the help entry for promote_type, would probably be pretty bamboozled when they see promote_type(Int8, UInt16) = UInt16 or promote_type(Int16, Float16) = Float32 or even (new one) promote_type(Int64, Float16) = Float16.

It looks like it is time to change the documentation for promote_type. I think saying something along the lines of "promote_type represents the default promotion behavior in Julia when mathematical operators are given arguments of differing types, and generally tries to return a type which can at least approximate most values of either type without widening the type." would be better, as long as the really counter intuitive cases are mentioned as examples to avoid bugs.

A smallest_containing_type (or widen, if you prefer) function would be very useful to preserve the old behavior of promote_type. I think several people, myself included, must have read the documentation of promote_type and assumed it was such a function.

Exactly, I am just arguing that promote_type does what "promote_type actually does" because one of its main uses is to be called by functions to obtain the right container type for an operation with mixed operands. So yes to everything.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

StefanKarpinski picture StefanKarpinski  Â·  3Comments

omus picture omus  Â·  3Comments

arshpreetsingh picture arshpreetsingh  Â·  3Comments

i-apellaniz picture i-apellaniz  Â·  3Comments

tkoolen picture tkoolen  Â·  3Comments