I think we should revisit disallowing ==
on F32
and F64
(and therefore pattern matching on them too)
main reason is that in the static dispatch world I'm not sure if it's feasible without sacrificing other goals
in static dispatch, we need a Num.equals
function and that's what a == b
will desugar to
when a
and b
have the type Num
the obvious type for Num.equals
is Num(a), Num(a) -> Bool
if we need to make the type be "not a F32
and not a F64
but Dec
is okay" then we need to make the number type system much more complicated than it is today
which I don't think is a cost that's worth the benefit of disallowing equality for floats
it would have to be strict bit equality right?
I'm setting that aside for now
it's more that we'd previously decided to disallow it, partly because it's error-prone and also because of what happens when you try to use infinity, -infinity, and NaN in dictionaries and sets
so, one idea would be:
Num.equals : Num(a), Num(a) -> Bool
crash
yeah it's goofy
yeah i kinda hate that last bullet point
I'm open to suggested alternatives haha
can we just have a hard coded prohibition on dicts and sets around prohibiting fp
it's the same problem as Num.equals
I think that's better than handing devs a footgun
how do you represent it in the type system?
Anthony Bullard said:
I think that's better than handing devs a footgun
that's why we originally disallowed this :smile:
I'm not saying representing it at all.
I definitely think it's a smaller footgun with crash
than with it doing what happens by default with nan in dictionaries/sets
Just a hard coded "i'm sorry dave..."
at compile time?
Yes with a clear explanation of why
we could do a warning I guess
NaNs are relatively easy to create especially through serialization/deserialization
error seems weird, like it's a type mismatch but not a type mismatch :sweat_smile:
Infinity not as much
i don't think it would be a type mismatch,
but at the very least a warning
the counterargument (which I do think is reasonable) is that if someone's using floats over the default of Dec
in general, it's because their use case needs hardware-accelerated performance more than it needs footgun avoidance, and so even knowing that floats are full of footguns they are making the choice
and so blocking them from using floats in these ways is just hurting them and not really helping them
under that design philosophy, the purpose of the crash
in dictionaries and sets would be the same as crash
on integer overflow: if this ever happens, you have 100% for sure entered a broken state, and being loudly broken is better than being silently broken
I agree, so maybe a warning is the appropriate thing
well warning just means error that doesn't block you from running
but warnings return nonzero exit code and they're intended to convey "you should 100% fix this at some point, but your program isn't necessarily going to crash just because you didn't fix it"
If the assumption is floats are used presumably for performance, why not moving them out from the set of good friendly numbers that lack NaNs, Infinities and variable precision? Yes, it would break generics over usual numbers. But floats seem to be too specific
because then you can't have like my_float + my_other_float
that's not a realistic option :sweat_smile:
a general theme with floats has been me trying to make them less error-prone and then gradually realizing that attempts to make them less error-prone also make them less usable for the thing they're actually good at, which is high-performance fractional math
and at this point I'm leaning towards the actual answer being "use Dec
if you want reliability, and if you want hardware-accelerated performance and footguns, use F32
/F64
and be aware that here there be dragons"
none of the experiments we've tried to make floats less error-prone have turned out well imo
but offering a non-error-prone alternative has been great and is unaffected by what we do with floats
because then you can't have like
my_float + my_other_float
.
Introduce a sister of Num
, FloatNum
and call it a day :smile:
that doesn't work
We could have Floating point operators
numbers all need to have the base type Num
and a different type parameter
And then they can live on an island
Anthony Bullard said:
We could have Floating point operators
yeah people love that about OCaml :joy:
Like .+, .-, etc
i'm just exploring the spaxe
yeah I hear ya
It's clearer what you are getting into
There is rational behavior over here, and floats over there
yeah, but like another thing I'm kind of tuned into here is that we're all coming at this from a web dev perspective
in my language i tried to get around it by only having DEC64 as the number type
I'm pretty sure if there were a Roc game dev in here they'd be like "wtf don't do that to me, I have to use floats all the damn time and I don't have a choice about it, why are you trying to make my life harder just bc I'm a game dev"
But apparently a lot of people like to shit on DEC64 and i didn't have the time to dig into the specific claims
GPU-powered native UI applications are in the same boat; F32
is what the GPU wants
like probably if you're making a web app, more often than not just using Dec
everywhere is the better choice, but Roc is aimed at a broader set of use cases
Richard Feldman said:
I'm pretty sure if there were a Roc game dev in here they'd be like "wtf don't do that to me, I have to use floats all the damn time and I don't have a choice about it, why are you trying to make my life harder just bc I'm a game dev"
Roc is going to be high perf, but game dev level performant? i mean outside of Love2D style?
game dev
yeah, I bet they love equality checks
Richard Feldman said:
GPU-powered native UI applications are in the same boat;
F32
is what the GPU wants
this is the real stickler
Anthony Bullard said:
Richard Feldman said:
I'm pretty sure if there were a Roc game dev in here they'd be like "wtf don't do that to me, I have to use floats all the damn time and I don't have a choice about it, why are you trying to make my life harder just bc I'm a game dev"
Roc is going to be high perf, but game dev level performant? i mean outside of Love2D style?
people make a living on Unity games written in C#, and we should be faster than that
Ok, floating equality is usually done with a fixed precision value?
I mean, approximate equality, right? Also, warnings per equality check?
in most languages with floats, equality compiles down to the CPU instructions for float equality, which has these semantics (as defined by the IEEE-754 hardware standard):
-0
and 0
are considered equalNaN
is considered unequal to everythingAnd in the warnings you would see "it's an approximate equality with x tolerance. If you want bit eq, use this, if you want different tolerance so that"
I shouldn't have said "warning" earlier - let's just say "compiler error"
if we give a warning, or special compiler error, for using floats with equals, to me this is basically equivalent to changing how number types work without actually making the change, except much hackier
I don't think we should do it
I think a reasonable comparison is with integer addition
we don't warn you at compile-time if you do a + b
even though that can be a runtime crash if it overflows
like we could say "you should consider using checked addition so you can handle overflow errors properly, or saturating addition if it's ok to round off to the nearest high number"
and force you to do that everywhere
but we who are not doing game dev or native GUI development in Roc know that this would be super annoying and make us not want to use the language, much more so than the errors it would prevent
so we wouldn't support that change
and I don't think we'd support errors for floats either if we were forced by our use case to use them all over our code base
I see. My take is that it's annoying searching for the place where two floats were compared if there was a bug. Especially in case of generic functions. But I lived like that and haven't died yet
If we give a footgun, I think it makes sense to provide a pair of decent shoes. A linter? Does roc already have a debug assertion function?
expect
is basically Rust's debug_assert!
I definitely would like to never have a linter in Roc
at least not the traditional kind that looks for generic "issues"
as opposed to project-specific ones where the end user sets rules like "hey we're trying to move away from Foo, so no new uses of it allowed!"
Once the roc parser is in the wild, community driven linters will be a matter of time.
But speaking of linter, I mean more clippy than eslint
I understand the sentiment that linters have slipped into style checkers. As a result you have purism in the name of democracy (yes, oxymoron) and part of the team go with "whatever" and the other hate unreasonable rule they regularly face that mostly hinders development.
But the roc linter can concentrate on heuristics of correctness and not style. So it might be a tool, not constitution.
Richard Feldman said:
because then you can't have like
my_float + my_other_float
Can you explain this more? I thought with static dispatch as long as they had add : F32, F32 -> F32
then it would be ok to use +
Anthony Bullard said:
And then they can live on an island
Is this because they're floating... thanks Dad :wink:
Richard Feldman said:
and at this point I'm leaning towards the actual answer being "use
Dec
if you want reliability, and if you want hardware-accelerated performance and footguns, useF32
/F64
and be aware that here there be dragons"
I think this is a very reasonable approach.
The "there be dragons" part is something we can also help with a lot more than in every other language where I assume these also exist.
I'm not sure about the "help a lot more than" -- because we're talking about runtime issues.
I only hope that dragons won't be implicit
Luke Boswell said:
Richard Feldman said:
because then you can't have like
my_float + my_other_float
Can you explain this more? I thought with static dispatch as long as they had
add : F32, F32 -> F32
then it would be ok to use+
oh you're right, sorry - that particular example would work, but you couldn't have (for example) the number literal 4.2
going from Frac(a)
to F32
anymore (or else you couldn't have it for Dec
either)
number literals rely on all of the number literals having the same nominal type - Num
- and then only the type variables change when you use them in more specific ways
The idea of pulling F32
and F64
out from Num
is an option right? They're still in the stdlib and available for use if you need performance, but you know they're not the safe default options.
We could still have special handling support for them like 43.0f64
we could, yes, and then Roc will never be a C# competitor for game dev :joy:
or a JS competitor for cross-platorm native GUIs etc.
I think a big part of the problem here is that when I was originally thinking about it, I was thinking about it as if people are going to use floats for performance in just like one hotspot in a large code base that really needs a little extra juice
but I don't think that's right
I think it's actually more like "my whole use case is floats, and if Roc's ergonomics around floats are terrible then I just will not use it for my use case"
so I think a better mindset is like "make it really really ridiculously easy to not use floats if you don't have to, but if you do have to, make the experience as nice as possible"
Maybe this is a silly question.. but why are they more ergonomic in Num? Is it not casting between number types, like when adding an integer or similar?
which I do think includes crashing when a NaN
would get inserted into a set or dict, instead of making the set or dict start quietly behaving in ludicrous ways
so like if I want to write the number 4.2
, just like that, and be able to use that exact syntax for an argument to a function that accepts a Dec
and also a function that accepts a F32
, then either they both need to be a Num
nominal type (with different type parameters) or else we need a fancier type system just for numbers
Ah ofc. I forgot about the polymorphic type things.
I'm only a newish, hobbyist game dev, but my impression is games people would generally also not want exact float equality. For example, the implementation of Vec3 equals in Unity has an epsilon range
IIRC there was some prominent bug in Minecraft related to using float equality instead of a <= guard. Which is the kind of thing where the consequence is youtuber lore instead of someone losing money, but game devs get got by floats too
yeah the problem is I've heard it's like "sometimes you actually do want it and other times you want to avoid it"
Richard Feldman said:
numbers all need to have the base type
Num
and a different type parameter
Given anything can implement plus and such in the new compiler, is this still strictly needed?
Richard Feldman said:
GPU-powered native UI applications are in the same boat;
F32
is what the GPU wants
Actually gpus really prefer bf16, tf32, or fp8. Fp32 is pretty outdated on gpus nowadays.
Richard Feldman said:
which I do think includes crashing when a
NaN
would get inserted into a set or dict, instead of making the set or dict start quietly behaving in ludicrous ways
Honestly, if we are specially casing flaots anyway to do this, just ban hashing floats in general. I don't think there is a generic way in roc for a dictionary to check for this. So it has to be special cased.
Richard Feldman said:
yeah the problem is I've heard it's like "sometimes you actually do want it and other times you want to avoid it"
I think it is 99% of the time you don't want it, but some hyper optimized code has uses.
banning floats from having hashing has the same problems as banning them from having equals
like basically if we want to have floats still be able to work with number literals but not support equals or hash, we have to change the Num hierarchy to something like this:
F32 : Num({
size : Fraction(Binary32),
has_nan : [Yes]
})
F32 : Dec({
size : Fraction(Binary32),
has_nan : [No]
})
U64 : Num({
size : Integer(Unsigned64),
has_nan : [No]
})
Num.equals :
Num({ size, has_nan : [No] }),
Num({ size, has_nan : [No] })
-> Bool
Frac(size, has_nan) : Num({
size : Fraction(size),
has_nan,
})
Num.div :
Frac(size, has_nan),
Frac(size, has_nan)
-> Frac(size, has_nan)
so the type complexity and learning curve for all numbers goes up, compile times get some amount worse because we have this extra stuff to check, and the payoff is that we have disallowed direct float equality in favor of having to do the (x >= y and x <= y)
trick
How would a dict crash when used with floats?
I think it has to be some sort of special cases mechanism
I think we should just special cases this
Simply and am extra of condiction that if a hash resolves to a float create a compilation failure
Like have it just happen after resolution
That or have it crash at runtime (but compile time error is a better experience)
Like I think should just not represent it in the type system at all. Totally a special case
this is interesting: https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/4
Yeah, reading some of that thread reinforces my opinion. We should just generate a custom compile time error if someone calls .hash
on a float.
If a user wants float hashing, they should wrap the float in a custom type. The best part of this is that the special type also, can implement all the math functions and be nice enough to use.
And I think this is an odd enough edge cases that it is worth making a special error for after type resolution. I don't think it is worth truly representing in the type system.
Also, I guess if num was a more standard type instead of a magically dynamic type, this could be dealt with in pure roc by calling Num.hash
, which would unwrap the outer type and then call Frac.hash
, which would unwrap and call F64.hash
, would either call crash
or a new compileTimeError
builtin. Frankly, you could just leave it as a header with no definition and that would technically work as a compile time error.
But I don't think num has true unwrapping of the outer type. Instead, it magically works for different multiple types.
Brendan Hansknecht said:
Yeah, reading some of that thread reinforces my opinion. We should just generate a custom compile time error if someone calls
.hash
on a float.
I'm on mobile and don't have a quick explaination, but in general I don't think we should do this
this is basically saying "we should introduce the concept of secret type mismatches that aren't represented in the type system so you can no longer ever tell from looking at two types whether they're going to have a mismatch"
it means things like a package can publish a new version with identical type signatures, as a patch release, and now it breaks your builds because it secretly started using hashing somewhere deep behind the scenes and you were passing it a float
Take a look at #ideas > Do we need Num anymore? I think we may be able to do this more properly.
if passing a particular type to a function can cause a type mismatch, that needs to be reflected in the type.
Last updated: Jul 06 2025 at 12:14 UTC