5 ms·
I'm curious why you characterize it as a "punishment"? Do you actually think that is why anyone designed the system this way, as some kind of aversion therapy j
by lambda 12y ago
I'm curious why you characterize it as a "punishment"? Do you actually think that is why anyone designed the system this way, as some kind of aversion therapy just to get you to avoid using floating point numbers?
All Rust is doing here is implementing IEEE 754 floating point semantics as specified, and implemented in lower level hardware. Part of the IEEE 754 specification that you need to deal with if you want to use floating point numbers is NaN, which represents not an actual value but the absence of a value, an indication that your computation did something that could not be represented. Because NaN is one of the possible values of a floating point number, a typesafe interface for comparing floating point numbers must, by definition, be a partial function; and as a partial function, it cannot be relied upon for implementing sort.
The Rust way of doing this does not punish you, it just ensures that you actually think about, and deal with, edge cases like this. While that can seem cumbersome in a small, one liner example like the above, or can seem restrictive when you're writing a simple personal project and you know that you will never encounter a NaN and so you just want to sort the values without thinking about that, it can be quite valuable when programming in the large; when you are working on a program larger and more complex for any one person to know and reason about the whole thing, type safety allows you to encode certain constraints in the type system that ensure that you don't make mistakes.
For instance, if you write code that depends on sorting a list of floating point values in Python, like in your example, and write all of your unit tests and design using non-NaN floating point values, then use a third party library that winds up producing a NaN value, you are likely to be quite surprised by the outcome:
>>> nan = float('nan')
>>> xs = [1, nan, 0, 3, 5, 2]
>>> sorted(xs)
[1, nan, 0, 2, 3, 5]
Now, deep in the middle of production code, that result might not be so apparent. Most of the values that you care about are ordered correctly; but eventually you'll hit the fact that the 1 is sorted before the 0, so you'll have some strange, hard to reproduce bug, that depends on the precise ordering of the original array.
What Rust is doing is not punishing you, but instead just making you make that decision about what to do about such a case up-front, before you accrue that technical debt that comes to bite you later on.
There are several possible ways you could deal with it; one is the one you mentioned, where you just define some way of comparing NaN so that you now have a total order. Another would be to panic any time you try to do an undefined operation like comparison on a NaN. Or you could work with a type that is restricted to non-NaN values, and deal with the issue only at the boundary of components which convert between arbitrary floating point values and your restricted subset (and any operations that may produce NaN values).
In order to not have to write those cumbersome sort expressions by hand every time, if you need to work with floating point numbers with one of the non-standard semantics described above, you could define any of the above behaviors by creating a newtype the wraps floats but provides the semantics you want. Since they are static types, there will be no overhead on the values, they will just be represented as floats; you may have some overhead on your checked operations that wrap the underlying float operations, but that the price you pay for going with semantics which are not the standardized IEEE 754 semantics as implemented by the hardware. In a larger project, if you need such a type, it's not all that much work to just define that type once with the semantics that you want, and then just use that everywhere rather than using one of the native floating point types.
So, what Rust is providing is a type-safe, low-overhead implementation of IEEE 754 floats, without providing certain conveniences that would make your life easier when dealing with a subset of them but cause problems when working on the full range of values.
Can it be convenient to accrue technical debt in order to get things done quickly? Sure. Shell scripts are a classic example; almost every non-trivial shell script will have some kind of quoting bug, delimiter bug, confusion between arguments and flags if an argument value ever contains a "-", or the like. But because they are familiar and allow people to get things done quickly, they can be really useful for little one-off hacks, especially when you're working with data that you know is simple enough not to hit one of those edge cases, like filenames where you know that none of them contain spaces.
However, you need to be really careful about that sort of thing. That kind of quick and loose reasoning can quickly come to bite you if it gets deployed in production in an uncontrolled or even hostile environment. All of as sudden, the things you thought could never happen will happen. I've seen a seemingly innocuous shell script for cleaning up a few particular types of files turn into an "rm -rf *" due to a bug in handling of spaces in filenames (and yes, an actual customer lost actual data due to this bug).
So, is Rust appropriate for that kind of fast-and-loose exploratory programming that the shell or dynamic languages like Python allow you to do? No. If I were working with known inputs, interactively, where I could easily tell that I didn't have NaNs and could check the outputs to make sure they were sane, I would choose Python and numpy, or Julia, or something of the sort that was more appropriate for rapid and loose prototyping.
But for software that will be deployed in the wild, where I need to write modules that will work with values provided by other modules that I don't control, or the like, making you think about this kind of thing up-front can help you avoid having weird, obscure, hard to debug problems, or even security vulnerabilities, down the line.
- losvedir 12y ago> Or you could work with a type that is restricted to non-NaN values, and deal with the issue only at the boundary of components which convert between arbitrary floating point values and your restricted subset (and any operations that may produce NaN values). How often do NaN's appear in practice? I think it makes perfect sense to handle IEEE 754 floats like this, but maybe rust should follow your suggestion here and provide a new totally-ordered float. Maybe `f64` should _be_ this totally-ordered float and IEEE 754 could be imported if you need to use that one?
- twoodfin 12y agoThat would likely make operations on the default float slower than they need to be. After all, the CPU is probably implementing IEE 754: How do you handle it when a NaN bubbles up from below?
- brohee 12y agoAnd what should be the result of a divide by 0 in your magical totally ordered float type?
- EugeneOZ 12y agoWhen you write programms for robots, you can't afford such "maybe" things. I think Rust will be perfect for robots.
- alextgordon 12y agoBob needs to sort an array of floats. Bob tries xs.sort() Bob gets an error message. Bob googles "how to sort an array of floats in Rust". Bob pastes in xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Less)) Bob continues on his merry way. No safety has been added, no technical debt has been avoided. It's not any less "quick and loose". The need to sort arrays of floats doesn't disappear simply because the Rust designers will it to. The code will still exist, but it will be longer and less maintainable. This is what I mean by punishment. If anything, Rust has given you a false sense of security. The modules and other code you work with will still be handling NaNs incorrectly. My preference, all considered would be a .sort() that pushes the NaNs to the front or back (but is slightly slower), and a .sort_unsafe() that assumes no NaNs but is faster.