5 ms·
Perhaps in some cases, but how does that apply here? People won't stop needing to sort arrays just because the syntax is made heavier and obscure. It's fine t
by alextgordon 12y ago
Perhaps in some cases, but how does that apply here?
People won't stop needing to sort arrays just because the syntax is made heavier and obscure.
It's fine to punish people for coding in the wrong way, but here I have to accomplish a task (sorting the array) and so the punishment serves no purpose at all except for making my experience worse.
- Ygg2 12y ago> It's fine to punish people for coding in the wrong way, but here I have to accomplish a task (sorting the array) and so the punishment serves no purpose at all except for making my experience worse. Then Rust way is fine. It's merely 'punishing' you for not thinking things through at compile time (that floats are sortable), instead of at run time (dependent on input). Maybe you prefer to have fail dynamically, and that's fine, but that's preference, and the goals of Rust do not align with it. If you find relying on it often, why not write a macro? So: xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Less)) becomes: sort!(xs) Also honestly, if this is a very common use case bring it up on Rust forums maybe people will add a macro. They added macro for generating N elements for a vector.
- twic 12y agoWhy a macro? Why not just define that comparator as a function? fn value_nans_last<T: Float>(a: &T, b: &T) -> Ordering { match (a, b) { (x, y) if x.is_nan() && y.is_nan() => Ordering::Equal, (x, _) if x.is_nan() => Ordering::Greater, (_, y) if y.is_nan() => Ordering::Less, (_, _) => a.partial_cmp(b).unwrap() } } xs.sort_by(value_nans_last);
- Ygg2 12y agoI haven't done sorting, so no knowledge of sort interface in Rust, but maybe so it works across different sortable collections?
- pcwalton 12y agoThere's no need to use a macro: you can abstract over different kinds of sortable collections using a trait. (That said, there's no real reason to generalize IMO: by far the most common case is sorting slices.)
- sanderjd 12y ago> If you find relying on it often, why not write a macro? Why not a function? I think people turn to macros in rust sometimes because of the necessity to think about the semantics of function calls. Should I use references or values? What are the trade-offs? The amount of thinking you have to do to perform a good "extract method" refactoring is one of my least favorite things about the language, but falls out of some of my favorite things about it. All in all, I think it's worth the trade. edit: Expanded a bit.
- lambda 12y agoI'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.