Julia: Change printing of Float32/Float16

Created on 18 Jun 2014  Â·  20Comments  Â·  Source: JuliaLang/julia

Having mucked around in the float printing code quite a bit lately, I found it unfortunate that Float16 is more lame than Float32. It's also unfortunate that there's no literal syntax for Float16.

I'd like to propose the following:

1.0  #  => Float64
1.0f32  #  => Float32
1.0f16  #  => Float16

Currently, Float32 is printed as 1.0f0, which, while visually indicative of its type, doesn't mean anything more (the f0 at the end doesn't indicate anything). Using 1.0f32 instead gives a hint to the bit-size of the float you're dealing with, along with allowing a natural extension to other float types 1.0f16, 1.0f80, 1.0f128, etc. The other advantage is it could allow literal syntax as well for different floats, and we all know how great it is to have literal syntax for types. I'm not sure if changing 1.0f0 to 1.0f32 would be breaking or not (does anyone rely on how Float32's are printed?), but we could always parse 1.0f0 as Float32 for another release if need be.

Is this really that important? Maybe not. Would it provide a little more polish and consistency? I think so.

Most helpful comment

Or of course a f16"1.0" string macro.

All 20 comments

Just a note: the f0 in the end means a power of ten, the Float32 equivalent to e0, e.g.

julia> 10f0^10
1.0f10

The f0 does mean something. The 0 is a power of 10 and f replaces e.

Ah, thanks for the clarification guys. Not sure how I got that mixed up. I guess my proposal wouldn't really work then.....hmmm.

Ok, I figured out where I went wrong. C/C++ has 1e-45f as float literal syntax. I think I got confused because I interpreted Julia's syntax as simply 1e-45f0. Which comes to my new suggestion:

1e-45 || 1e-45f64  # => Float64
1e-45f32  # => Float32
1e-45f16  # => Float16

Similar idea, but exponentiation would always use e, whereas the f is solely used for syntax literal specification.

That conflicts with implicit multiplication by numbers. It's also hard to generalize e.g. to fixed point.

Oh drat. Seems there's not an easy solution here.

Ok, one more. It's actually been suggested before through a combination of #25 and #964.

1.0  # => default Float64
1.0::Float32  # => Obvious
1.0::Float16  # => Again
#exponentials
1e-45
1f-45 or 1e-45::Float32 # I'd be in favor of getting rid of the 'f' exponential syntax since it doesn't generalize well
1e-45::Float16

And as #25 suggests, I think we go further and do the same with Int8, Int16, and Int32. Any time you're working with those you're constantly doing typeof(ans) to make sure you got the right type.

That can't work either:

julia> 1.0::Float32
ERROR: type: typeassert: expected Float32, got Float64

It might work if there were more concise syntax for convert, but that's still limited by what the parser is able to parse. For example, you can't write BigFloats without using a string or extending the parser.

I think there's a case for having 1.0::Float16 actually be syntax in any scope for convert(Float16,1.0). It's already what it does on top of typeasserts in functions, so I don't think it's too crazy to expect a call to convert when used. I feel like that's been brought up in #1470, as well as #1090, for return type declarations.
Could the parser handle something like that?

1.0::BigFloat
1.0::Float64
1.0::Float32
1.0::Float16

Or maybe I'm not understanding the parsing problem. Is it that we don't want to litter code with all these calls to convert?

:: never calls convert. It either calls typeassert, or adds a declaration that will later call convert on assignment. I think having :: sometimes call convert will confuse people to no end, especially since this syntax is already a bit subtle.

The parsing problem is that if I write <long digit string>::BigFloat, the long digit string may have already been parsed as a Float64, rounding off the extra digits.

Part of the subtlety and confusion is that using :: in a declaration _directly leads_ to calls to convert.
So yes, definitely the syntax is subtle because sometimes it's just asserting and other times it's actually leading to calls to convert.
All the more reason to make it consistent. The behavior, in a future world, could be:

f(a,b)::Int64    # => return type declaration; I know I'll get an Int64 back; :: has forceful behavior
1.0::Float32   # => float literal syntax that makes sure I get a Float32; :: has forceful behavior
function foo()
  x::Int16
end     # => declaration syntax works as it currently does, leading to calls to convert; :: has forceful behavior

With BigFloat parsing, is it just a "hard to get right" problem? Or not really possible problem?

As far as I remember (don't quote me!), the parser itself treats most numbers as numbers directly (using the femtolisp types) instead of just postponing this step to Julia, which makes a potential BigFloat literal something problematic.

The big float literal is problematic because not every floating point number is machine representable. You need a default float type in the parser because otherwise you would have inconsistent behavior with rounding if you still want to have floating point numbers as literal values in the ast.

"Forceful behavior" is not a meaningful concept. Right now :: does two things. That change would add a third: turning 1.0::Float32 into convert(Float32, 1.0). If we want to add special syntax for convert (and possibly use it for declarations too) and have :: always mean typeassert, that would be fine with me. But typeassert is here to stay.

Parsing can be handled; we are able to parse BigInt literals for example. But floats are harder because it is customary to write Float64 literals with extra digits to be sure you get the closest representable value to the one you want. We would need some kind of syntactic cue, but I would be against adding yet another meaning to :: for this purpose.

I think a lot of the confusion about x::T stems from the fact that it is sometimes a type assertion and sometimes a variable declaration – and it depends on if you're in global or local scope. This is exacerbated by expectations from static languages.

We could add @float16_str and @bigfloat_str, but other than that I'm not sure what to do. I don't think it's so important to have Float16 literals. BigFloat literals are probably more useful.

Is there any possibility of a Float16 literal notation being introduced? It seems inconsistent to have literals for the other types but not Float16. Also, I would argue that Float16 has become more important in the past 5 years, with increasing interest from machine learners and (in my case) atmospheric modellers.

What about 1.0g0 (suggested by @milankl, continuing the e, f... pattern)?

It would be a breaking change, so not until Julia 2.0 at the earliest.

One stopgap in the meantime could be

struct Float16Scalar
    exp::Int
end 
Base.:(*)(x, u:: Float16Scalar) = convert(Float16, x*10.0^(u.exp))

macro g_str(sn)
    Float16Scalar(Meta.parse(sn)::Int)
end

julia> 1g"0"
Float16(1.0)

julia> 2g"10"
Inf16

Inspired by Unitful.jl

Or of course a f16"1.0" string macro.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

omus picture omus  Â·  3Comments

tkoolen picture tkoolen  Â·  3Comments

arshpreetsingh picture arshpreetsingh  Â·  3Comments

StefanKarpinski picture StefanKarpinski  Â·  3Comments

dpsanders picture dpsanders  Â·  3Comments