Parametric bits

Float{64} would make a lot more sense than Float64, no? Julia exports at least 13 float/integer types, and with parametric types it could cut down to three (signed, unsigned, and float). Maybe something to consider for 2.0?

What do you need this for? For eg (hypothetical code)

function foo(x::Float{T}) where {T}
    print("I have $T bits!")
end

you can define a trait, or use size(Float64) * 8.

This would imply that Float{13} is just as valid and usable a type as Float{64}, which it’s not.

3 Likes

Hmm maybe Float{6} then, where 64 = 2^6?

Why does Float{64} make more sense than Float64? Having a number in the name is not a reason to replace that number by a type parameter.

All the exported types requires completely different handling from each other (they are all different hardware types). Parametric type is for dealing with types that are the same with each other apart from the type parameters and this is not the case between the number types you suggests. Making them parametric type does not help writing any code and simply confuses the user about their properties/relations.

Also, reducing the export is not a goal on it’s own, or basically all type in Base would have been spelled as IntegerType{:Int64} etc.

5 Likes

Doing this would make total sense if there was a general mapping from N bits to a floating point layout, and there’d be no issue mapping Float{64} to hardware operations where possible. Unfortunately I don’t think any such mapping exists that would include Float64 and Float32 as special cases, and it may not be possible to do this in a sensible way, in which case it’s better to view them as completely distinct types.

Julia can’t do anything here, but that’s largely just historical contingency rather than it being an inherently unreasonable idea.

4 Likes

Having a well-factored API is an explicitly stated goal, and the official reason why we can’t have underscores

That is not reducing exports at all. Reducing exports when not needed is the mean to achieve a well-factored API, together with having easy to remember, widely accepted and concise names etc. It is definitely not the goal itself and doing what you are suggesting does not make a better API. As I said, you can arbitrarily reduce the export size using type parameters. In fact, you can remove almost all function export from Base and replace them with

julia> struct BaseFunc{S}
       end

julia> (::Type{BaseFunc{S}})(args...; kwargs...) where S = getfield(Base, S)(args...; kwargs...)

and that probably cut down the export from base by an order of magnitude. Such changes are simply changing the spelling, just like Float64 and Float{64}.

Basically, putting the information into the type parameter does not on itself bring any benefit and cutting down the export from 13 to 3 is not an argument for anything. You must instead look at the properties of the types you are trying to consolidate and see what a different it’ll make for the user/implimentation. AFAICT, most (all?) code either care about the property of the abstract type (Unsigned, Signed, AbstractFloat) which includes other types as well, or they care about the exact concrete type since, as I said, these are different hardware types. Adding the type parameter doesn’t help for either of these and merely hide the fundamental difference between the types and mislead people to over-generalize.

Also, underscore is something completely unrelated to this.

1 Like

By comparison, it would make some sense to have parameterized UInt and Int types since there is a completely standard representation and behavior for unsigned and signed two’s complement integers. However, while that’s the theory, in practice, using integer sizes that are not what the hardware supports directly is not recommended. It will be slow and will most likely hit LLVM bugs. Exporting types that suggest that using integer types with arbitrary bit sizes seems like doing our users a disservice.

Note that in a somewhat related area we have ComplexF32 and ComplexF64 as convenience abbreviations for Complex{Float32} and Complex{Float64}, even though the Complex type is truly generic. That’s because these are the most common and hardware supported complex types. So even if UInt64 were defined as UInt{64}, we’d probably want a convenience alias for it anyway, since it’s by far the most common integer type that you want.

2 Likes

I would have liked for the precision of BigFloat to be a type parameter, rather than a mutable global state.

But I’m guessing there’s something in the internals of GMP that would make this hard to implement?

Yes, implementation aside bigfloat of different sizes are similar enough for this. Unless the runtime or the user can take advantage of it though, it should just be data (field or global) other than in the type.

But the choice between “size as type parameter” and “size as dynamic global setting” is a false dichotomy. There is also the option of passing it as an argument the the BigFloat constructor and having operations on big floats do promotion based on the precisions of their arguments (i.e. the result has the max precision of its arguments). We can already construct BigFloat values of a given precision with the precision keyword argument, the only problem that remains at this point is that the precision of operations on BigFloats is taken from the global setting, not from the precisions of the arguments:

julia> big_pi = BigFloat(pi, precision=1024)
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067982148086513282306647093844609550582231725359408128481117450284102701938521105559644622948954930381964428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273724586997

julia> big_e = BigFloat(â„Ż, precision=2048)
2.71828182845904523536028747135266249775724709369995957496696762772407663035354759457138217852516642742746639193200305992181741359662904357290033429526059563073813232862794349076323382988075319525101901157383418793070215408914993488416750924476146066808226480016847741185374234544243710753907774499206955170276183860626133138458300075204493382656029760673711320070932870912744374704723069697720931014169283681902551510865746377211125238978442505695369677078544996996794686445490598793163688923009879312773617821542499922957635148220826989519366803318252886939849646510582093923982948879332036250944311730123819706841609

julia> big_pi*big_e
8.539734222673567065463550869546574495034888535765114961879601130179228611157304

julia> precision(ans)
256

Some previous discussion here:
https://github.com/JuliaLang/julia/issues/10040

2 Likes

The main hassle with having precision as a type parameter is that it will then invoke compilation for every new precision value. This can cause problems for cases where you do actually want precision to be set dynamically, e.g. I’ve written code for computing continued fractions by iteratively recomputing expressions in higher precision.

It’s also unnecessary to solve the main problem people have with the current BigFloat behavior—all we need to do is propagate precision from arguments to results during operations. The main hope from putting the precision in the type would be that the compiler could store BigFloats in registers for small enough precisions, but I’m not sure how realistic that actually is and how much better that would be than just being in L1 cache.

3 Likes

Imo exposing primitive type to users is already too much: primitive type foo 24 end will produce bugs everywhere, due to different parts of the compiler disagreeing on whether this is 3 or 4 byte (3 byte payload + 1 byte padding for alignment).

I think we could do without syntax / keywords for that and expose it in expressions only, just like :new (constructing instances of objects without calling inner constructors; this is impossible by writing julia source, but very possible by emitting julia expressions, e.g. via macros or generated functions. It is inconvenient for good reason: That way you can generate an object struct bar x end where x is nullpointer, and segfault on bad_bar.x instead of getting UndefRefError).

You can simulate this with

import Base: +

struct FloatBits{Bits, FloatType}
    raw::FloatType
end

struct IntegerBits{Bits, IntegerType}
    raw::IntegerType
end

struct UnsignedIntegerBits{Bits, UnsignedIntegerType}
    raw::UnsignedIntegerType
end

FloatBits{Bits}(raw) where {Bits} = 
    error("$Bits bit floats unsupported")
FloatBits{16}(raw) = FloatBits{16, Float16}(Float16(raw))
FloatBits{32}(raw) = FloatBits{32, Float32}(Float32(raw))
FloatBits{64}(raw) = FloatBits{64, Float64}(Float64(raw))

IntegerBits{Bits}(raw) where {Bits} = 
    error("$Bits bit integers unsupported")
IntegerBits{8}(raw) = IntegerBits{8, Int8}(Int8(raw))
IntegerBits{16}(raw) = IntegerBits{16, Int16}(Int16(raw))
IntegerBits{32}(raw) = IntegerBits{32, Int32}(Int32(raw))
IntegerBits{64}(raw) = IntegerBits{64, Int64}(Int64(raw))
IntegerBits{128}(raw) = IntegerBits{128, Int128}(Int128(raw))

UnsignedIntegerBits{Bits}(raw) where {Bits} = 
    error("$Bits bit unsigned integers unsupported")
UnsignedIntegerBits{8}(raw) = UnsignedIntegerBits{8, UInt8}(UInt8(raw))
UnsignedIntegerBits{16}(raw) = UnsignedIntegerBits{16, UInt16}(UInt16(raw))
UnsignedIntegerBits{32}(raw) = UnsignedIntegerBits{32, UInt32}(UInt32(raw))
UnsignedIntegerBits{64}(raw) = UnsignedIntegerBits{64, UInt64}(UInt64(raw))
UnsignedIntegerBits{128}(raw) = UnsignedIntegerBits{128, UInt128}(UInt128(raw))

+(x::IntegerBits{Bits}, y::IntegerBits{Bits}) where Bits = 
    IntegerBits{Bits}(x.raw + y.raw)

IntegerBits{8}(127) + IntegerBits{8}(127)