10 ms·
Dark side of ergonomics in Rust
- sanxiyn 8y agoReddit discussion here: https://www.reddit.com/r/rust/comments/8asb4i/dark_side_of_ergonomics/ https://www.reddit.com/r/rust/comments/8asb4i/dark_side_of_e...
- Wildgoose 8y agoI've only just started learning Rust but having spent 38 years programming I largely agree with his sentiments. Typing .unwrap() forces you to acknowledge that what you have may not actually be what you expect. Explicit flow control makes what is happening clear to understand and reason about without jumping about the code via exceptions. Type conversions should also be explicit, (and automatic type conversions would needlessly complicate the Type Checker in any event). I am sure there are places where "ergonomics" can be improved, but it is far more important to avoid the many (accidental) mistakes made by other programming languages. Rust is going to be around for a long time. So any mistakes made now will also be around for a very long time.
- zacmps 8y agoI don't think type conversions need to always be explicit, there just needs to be a lot of careful thought before they are. Automatic deref would be an absolute pain to live without for example.
- ben0x539 8y agoWould lack of automatic deref be less painful with a "->" operator equivalent, or just postfix deref? I don't want to say automatic deref is harmful, but a lot of the time I wish I could locally deduce more about the level of indirection.
- glandium 8y agoSpeaking of explicit conversions, it's always painful to have to do explicit conversions of integers. E.g. Have an u8 and want to use it as an index in a Vec? You have to cast as usize. That quickly becomes annoying.
- royjacobs 8y agoIf you need to do that a lot then arguably you'd be better off having a variable of 'usize' in the first place.
- thinkpad20 8y agoOn the contrary, I think that’s an argument against implicit conversions. u8 and usize have wildly different ranges, and treating them as the same could cause some maddening bugs. I can’t imagine there are that many places where you’d need to use a u8 as an index; if there were you could either wrap your data structure in a struct which accepts u8 instead, or use usize more instead... (Not that I’m claiming to know your code base or specific challenge or anything; I’m just speaking generally)
- userbinator 8y agoI think implicit widening is a good idea, but not narrowing --- expanding a u8 into a usize doesn't actually lose any information, but going the opposite way does.
- therein 8y agoDepends on the machine architecture really but I agree in principle.
- Gibbon1 8y ago> I can’t imagine there are that many places where you’d need to use a u8 as an index I have some firmware on a small machine, there aren't any arrays with more than two dozen elements. On the eight bit machine the the code originally ran on using a 16 or 32 bit int caused a lot of code bloat. You might not think that's a problem but consider the price difference between a processor with 64k of flash and 128k might be a dollar. Times 100,000 units a year. The above is why I'm not going to use rust anytime soon, because a rust binary size is about 4 times larger than C. That would add about $2-3 to the cost of the product. Or $200-300k a year for no real benefit at all.
- jeremyjh 8y agoI would argue Deref coercions are still explicit, because the trait implementations are explicit. There is not a magic mapping of types whose references can be coerced to each other, it is exactly the ones that implement Deref<Target=T>.
- zzzcpan 8y agoType conversions have to be explicit when they are lossy, otherwise it's just useless noise and cognitive overhead that can lead to a bug.
- ben0x539 8y agoI dunno, "newtypes" are a fairly popular patterns, and if they automatically converted between the base type and other newtypes of the base type, they'd not really be useful.
- k__ 8y agoCan't they have performance impacts even if they aren't lossy?
- Too 8y agoType conversions, even non lossy ones, can teach people to use the wrong type, in c++ it's very common to see people use a int in a for loop indexing an array when you should always use size_t for that purpose. This misuse is so widespread that people hardly even know that the size_t type exists. https://www.viva64.com/en/a/0050/ https://www.viva64.com/en/a/0050/ has some nice material about why this matters and the type of bugs this can cause.
- Gibbon1 8y agoPersonal opinion, heavy use of int is a code smell. Ada with 'range' probably gets this right.
- mastax 8y agoThing is, nobody was arguing for any of those things. This would be a much more useful article if it argued against a specfic real ergonomic proposal.
- emn13 8y agoNothing wrong with being cautious, especially with something as foundational as a language feature. It's quite hard to remove a feature that turns out to have unforeseen indirect consequences.
- zkomp 8y agoHmm yeah... I think I completely agree with this, the whole point of rust (for me) is safety and less surprises - the surprises tend to be misguided ergonomics. But is this a real danger now? Is the ergonomics initiative trying to make rust into JavaScript or Ruby? That would be unfortunate, languages should be different and the point of rust is sacrificing some ergonomic for safety and in the end more power...
- oblio 8y agoIs there any actual Rust ergonomics initiative that does what he’s saying? Or is he just talking about some generic possibility that isn’t actually happening?
- rhn_mk1 8y agoThere was one in 2017 [0], and work started back then is going to be continued over the year [1]. It's not clear to me if there are going to be any new ergonomics proposals though. [0] https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html [1] https://blog.rust-lang.org/2018/03/12/roadmap.html https://blog.rust-lang.org/2018/03/12/roadmap.html
- oblio 8y agoYou misread my comment, I think. I know that there are Rust ergonomics initiatives. My question was: is there anything bad like he described in his post in one of these initiatives? Or is he just tilting at windmills?
- the_mitsuhiko 8y agoThe process is open for anyone to submit proposals so nothing stops wacky ideas to end up in rfc’s. That does not mean they have a realistic chance of being accepted. Nothing wacky was even close to being accepted. The weirdest proposal that made it into rust was auto deref and I think that’s a question of balance. I am on the side of the advantages outweigh the disavantages and rust without auto deref would not be a sane language but maybe some other patterns would have emerged.
- brson 8y agoI think no. The stuff the op is describing is mostly not on the ergonomics agenda (I was thinking "strawman" while reading this), except for possibly limited automotic cloning for trivial types like `Rc`. It doesn't seem to describe the team's actual thinking about ergonomics. The Rust team is very aware of the importance of writing foolproof correct code in Rust (that's what it is designed for), and unless some major shift happens, I doubt they would make any major correctness sacrifices for the sake of ergonomics.
- jononor 8y agoAre any of these actually proposed to go into Rust? Or something like it?
- brson 8y agoI'm not really up to speed on what the Rust team is thinking, but basically no. These are not the kinds of things the Rust team would do to improve ergonomics. Correctness is forefront in Rust. The only thing I can think of like this op that has seriously been considered is automatic cloning for trivial types like `Rc`. The Rust team thinks really hard about correctness before making any decisions and isn't going to add any ridiculous footguns to the language unless there's some organizational catastrophe that puts monkeys in charge of decision making.
- phaylon 8y agoI'd say there are a couple of things that apply: * `?` can propagate values/short circuit control flow, but it also contains a hidden type conversion. The target needs to be a known type for it to compile. * A proposed `catch` construct will include implicit type conversion for its result value. Some things that came up in the past but met resistance: * Removing project structure from in-code to being defined by the filesystem. * Hiding `Result` types in signatures with special syntax. * Dropping immutable by default, though this was pre-1.0. Also, `Rc` and `Arc` are good examples for this. They do seem trivial, but having them auto-clone would mean: * Every newcomer who writes `fn foo(val: Arc<Bar>) {}` instead of `fn foo(val: &Arc<Bar>) {}` or `fn foo(val: &Bar) {}` will get an implicit atomic increment/decrement at every function call. * It also makes it a lot harder to reason about code if you want to use a clone-on-mutation-unless-not-shared strategy.
- hyperpape 8y agoI think RC examples are very good, but in the case of special syntax for Result, isn’t that a case where the two options are semantically the same, you just are wary of the less verbose/loud option (I have withoutboats’ post in mind here)?
- Fronzie 8y agoThe dislike of exceptions seems more common. It puzzles me a bit. For numerical software, exceptions work quite well: for performance, all allocations are in the constructors, so the RAII idiom fits naturally. Using exceptions saves from writing a great deal of error-handling, which easily can have errors itself. Has anyone seen properly-RAII-ed code where exceptions still have drawbacks ?
- panic 8y agoSay you have some code like, auto button = Button::create(); window.addSubview(button); button.setLabel("test"); button.setAlignment(Alignment::TOP_LEFT); button.sizeToFit(); If setLabel() throws an exception, you'll end up with a half-initialized button in the view hierarchy. For exceptions to work predictably, all mutation within a try-catch block should be captured in a transaction that can be rolled back. Then you don't need to worry about each function possibly throwing an exception. Either the entire block commits its changes, or it's as if the entire block didn't happen at all.
- ben0x539 8y agoWouldn't this be fixed simply by calling `addSubview` last (and designing your UI toolkit in such a way that you don't need to imperatively call `sizeToFit` after adding a thing to a container, but implicitly sizing things correctly during layout)?
- zaphar 8y agoAny solution that boils down to just get everyone who writes code you have to interact with to always write the code correctly is doomed to failure. Rust tries to make it impossible in the general cases to do the wrong thing. And gives you an unsafe escape hatch for when you need to do something the compiler won't allow. The result is vastly safer code because the compiler guards against the most typical human failings.
- camgunz 8y agoSometimes whatever `window` and `button` actually are have references to each other (ex: parent/children relationships) so it's possible -- in fact it's surprisingly common that -- there is no correct order. You need total knowledge of what can throw exceptions, and while the same is true for status codes, the raison d'etre for exceptions is shrinking the amount of error handling in your code. If you're checking for exceptions at the same rate you'd be checking for status codes, exceptions are pointless. But generally, your suggestion to change the architecture points to what the problem with these "ergonomic" changes engender. You more or less never have to rearchitect or refactor things when using status codes, and rearchitecting and refactoring are error prone and generally hugely fraught. The cognitive load of things like exceptions leads to lower productivity or lower quality, because you only have so many brain cycles and something has to give. I think ergonomic changes are helpful to get more dynamic programmers into stricter languages like Rust. But I think we should keep in mind that typing and boilerplate are really never our problems and prioritize accordingly.
- hexane360 8y agoThe main problem I see with this reasoning: The author provides some "extreme" examples of bad ergonomics, and posits there are milder examples of similarly bad ergonomics. But their examples aren't on the scale of "ergonomics", they're on the scale of "weakening language guarantees". Instead of writing an essay with (intentional) strawman examples and then saying "trust me this could happen with any new ergonomics" is much less persuasive than just bringing up any legitimate concerns on offending ergonomic features. This essay is kind of arguing against no one and everyone at the same time: No one disagrees that automatic unwrapping is a bad idea, but it's ridiculous to say that "I reach more ergonomics by accepting more programs and hoping they do what the author have meant, so I don’t have to bother the author with a compilation error." This isn't arguing against ergonomics, it's arguing against. . . I don't know, making Rust not Rust anymore?
- foldr 8y agoI don't see anything wrong with automatic unwrap in principle. It's not really much different from how Rust handles array indexing (where you don't have to explicitly handle the case where the array lacks an element at the given index).
- gamegoblin 8y agoRust provides two methods for array indexing. `my_array[i]` which panics if `i` is out of bounds, and `my_array.get(i)` which returns `None` if `i` is out of bounds. The same thing applies to `HashMap` etc. For the vast majority of cases, it is an non-recoverable logic error to try to access an out-of-bounds elements, so panicking makes sense for the more common, more terse `my_array[i]` syntax. For me, recoverability dictates whether or not to return `Option<T>` or `T-but-maybe-panic`. If a caller can reasonably recover (and in fact expects to get `None` some of the time!), then `Option<T>` is the right choice. If it is obviously an irrecoverable logic error, then maybe panicking is the right choice.
- foldr 8y agoYes, I'm aware of how Rust handles array indexing. I was just noting that autounwrapping wouldn't be any less safe than the existing behavior for array indexing using [].
- ensiferum 8y agoExceptions, errors and bugs are all different, have different semantics and subsequebtly different outcomes. Also the author is confusing noexcept with nothrow
- topspin 8y ago>> Also the author is confusing noexcept with nothrow I'm left wondering if that was an experiment to see if anyone would notice. I don't wonder about his point; most working C++ programmers don't understand the difference either. "...yet another keyword nobody learns to use."
- dom96 8y agoTo be fair, the C++ noexcept feature isn't as powerful as it could be. There are languages with compile time checked exceptions, personally I prefer that to propagating every exception manually.
- pjmlp 8y agoC++ had them, based initially on Modula-3's checked exceptions, it never worked quite right.
- fnord123 8y agoAIUI, this could be checked at compile time, but you could be calling a function that was brought in at link time so what's the point?
- AstralStorm 8y agoYou could add some extra metadata descriptor to a function at a cost of space and checks... (like some functional languages do)
- pjmlp 8y agoGood luck trying to convince C++ devs to add metadata to their binaries. One reason why we have always to come up with our reflection solutions was because of this.
- Someone 8y agoC++ adds metadata to binaries all the time, via name mangling (https://en.wikipedia.org/wiki/Name_mangling https://en.wikipedia.org/wiki/Name_mangling) noexcept is part of the function type since C++17, so it will be encoded in binaries (https://stackoverflow.com/questions/46798456/handling-gccs-noexcept-type-warning https://stackoverflow.com/questions/46798456/handling-gccs-n...)
- pjmlp 8y ago
- kybernetikos 8y agoNow I haven't programmed enough rust to know if there's a real problem here, but I worry about things like NLL. It means that the borrower checker accepts more programs which is good, but it also means that the model you need to have of its behaviour to correctly predict how it acts is a bit more complex. There's a trade off there.
- foota 8y agoI would argue that it's actually the opposite. To understand how borrows work right now takes more understanding of how the checker operates than with non lexical lifetimes.
- tinco 8y agoThe code in the compiler is more complex, but the domain is now more fully covered, so there is less understanding needed for the programmer. It now simply understands your intention, where before it would not. So previously you would write some code, be surprised that it does not compile, gain a thorough understanding of the limits of the borrow checker, write a workaround to appease the checker. Now you write the code, expecting it to be correct, and the borrow checker will agree.
- kybernetikos 8y ago> It now simply understands your intention, where before it would not. So previously you would write some code, be surprised that it does not compile, gain a thorough understanding of the limits of the borrow checker Much more important than it understanding your intention is you understanding its limits. If it understands your intention 10% more of the time, but it's harder for you to understand what's going on when it doesn't, then that is not something everyone is going to find simpler. Another word that gets applied to systems that try to guess at your intentions rather than operate according to an easily predictable model is 'magic'.
- tinco 8y agoYou would be right if it did guess, but it doesn't. It's not magic, I'm not sure if it still has limitations, but I think the idea is that it eventually does not have limitations. It's not magic when a compiler compiles your correct code.
- always_good 8y agoSpeaking of ergonomics, I seem to write a good deal of this for short-circuiting in Rust: let x = match x { Some(x) => x, None => return None, } Rust doesn't have any other way to short-circuit and it gets pretty tedious. I think Kotlin's named returns are most ergonomic of all. That way you can just short-circuit from anywhere whether in the middle of a fold or a deeply nested iteration. fn foo() { for x in xs { for y in ys { if y == 42 { return@foo 42 } } } } The other thing I'd like for other languages to steal for Kotlin is how everything has the .let/.apply/etc. methods. I'm rusty so this is more pseudocode, but these methods basically let you bring your own chaining to any value. let y = x.let { x -> x + 100 }.apply { x -> println(x) }.let { x -> x * 2 } Once you use it, it feels pretty silly that library authors in other languages have to deliberately design a chainable API for the same ergonomics. ...And infuriating when you want to chain some more in other languages but you've run out of chain context. Like when you use `.unwrap_or()` in Rust but still want to continue chaining on to the value. But can't because it's not in a container anymore with a chainable API. let x = maybe .map(foo) .and_then(bar) .unwrap_or(42); // ugh, want to apply a function to the result so // far but can't use `.map(to_base16)` because // it's been unwrapped into u32. Just some things from my wishlist for the next language someone makes.
- glandium 8y agolet x = match x { Some(x) => x, None => return None, } This implies your function returns an `Option`, and that `x` is an `Option` too. In which case you can just do let x = x?;
- always_good 8y agoWell, you may be short-circuiting for any reason, from any signature. For example, look a Swift's guard statements. It even narrows the type down-scope after you short-circuit. If you borrow in the `match {expr}`, Rust still thinks it matters when you're trying to short-circuit in the `None` branch and will complain about the borrowed lifetime. Though this is something #![feature(nll)] fixes. You can implement std::ops::Try for a custom data structure, but that doesn't help you in any of the cases where you just want to do some day-to-day short-circuiting without inventing your own data structure. I shouldn't have used Option, though. It was a bad example.
- akulbe 8y agoI'm new to programming in the last few years, so please excuse my ignorance here. When I hear/read the word "ergonomics", I think of proper posture and hand/head/eye position when you're working on a computer. What does the word mean when used in this context? Thanks. :)
- ben0x539 8y agoI think when people talk about ergonomics of a programming language, it basically means "convenience", but more dramatic. If your language offers a good way to express the problem, but it's inconvenient (too much typing to write it out, makes you think too much about inessential problems, fragile and needs to be adjusted when other code changes, ...), the total cognitive overhead (TCO? heh) might be higher than if you just used a different, worse way to express the problem, thus potentially making really cool language features completely useless because people won't want to use them.
- cesarb 8y agoThink of a tool, like a hammer or a screwdriver. Its handle must have the correct shape for a comfortable and strong grip; a handle that's too thick, too thin, or oddly shaped, will be hard to use. The tool's shape must not force you to hold it in a strange position. The tool's length must be adequate for the task; neither too long, nor too short. Now apply that to a software tool, like a compiler.
- currymj 8y agoit's kind of a subjective judgment. it's easier to define bad ergonomics, I think -- it's where the way part of the language is designed requires you to write very contorted, strange code to accomplish a task that ought to be simpler. you know it when you see it. in whatever languages you know, look for places where there's a lot of tedious repetitive typing, and deep nesting that seems frustratingly unnecessary. if you know Java, working with exceptions in Java has terrible ergonomics, bad enough that people come to resent the entire exception system even though it has many nice qualities.
- snarfy 8y agoI think most of the ergonomics issues can be addressed by improving the tooling without needing to pollute the language. An IDE could provide snippets and autocomplete for common patterns.
- ben0x539 8y agoThat helps with writing, but does it completely solve the problem with reading/code review?
- deleted 8y ago[deleted]
- phaylon 8y agoI'd say Rust's first line of solutions for this is macro_rules. They are structured, allow common patterns to be re-used in various places, can be reviewed and maintained in one place and apply to all uses, they can even be distributed via crates.io.
- finchisko 8y agoI'm just reading it, but already like the disclaimer. :D
- millstone 8y ago> Typing of the .unwrap() makes me acknowledge it could be None. You know, the effect "Oh crap, I better handle that too, right?". It forces me to fix my mental model, and not write the bug, instead of fixing the bug later on. How does this work for Mutex::lock()? Do you actually try to handle the case where taking the lock fails, because it has been poisoned?
- therein 8y agoFrom the documentation [0] for Mutex<T>: For a mutex, this means that the lock and try_lock methods return a Result which indicates whether a mutex has been poisoned or not. Most usage of a mutex will simply unwrap() these results, propagating panics among threads to ensure that a possibly invalid invariant is not witnessed. A poisoned mutex, however, does not prevent all access to the underlying data. The PoisonError type has an into_inner method which will return the guard that would have otherwise been returned on a successful lock. This allows access to the data, despite the lock being poisoned. [0] https://doc.rust-lang.org/stable/std/sync/struct.Mutex.html https://doc.rust-lang.org/stable/std/sync/struct.Mutex.html
- millstone 8y agoThe author argues that explicit unwrap() is good because it forces you to realize the value may be empty and think "Oh crap, I better handle that too." But the docs you linked say that "most usage of a mutex will simply unwrap these results" and not try to handle the case. So it seems like the explicit unwrap() here is not preventing any bugs, it's just adding noise. Wouldn't automatic unwrap be better, at least in this case?
- iknowstuff 8y agoAgain, it makes the programmer aware of possible issues and gives them a nudge to consider handling the error. This is very valuable.
- the_mitsuhiko 8y agoI personally switched to the parking lot crate which has mutexes that do not poison. I have never seen people need poisoning or handle it.
- thayne 8y agoI agree with the overall sentiment, that ergonomics should not come at the cost of less safety or correctness. However, two of the three examples, I would actually consider to be _unergonomic_. Specifically, empty/null values and exceptions. For small code examples they might seem ergonomic at first glance, but from esperience with large projects in scala, java, and c++. The actually add writing code more difficult (at least if you care about handling error cases at all). Regarding null, I think it is much more economic to work with an Option or Maybe type, with operations like `map`, `unwrap_or`, and pattern matching than checking if values are null all over the place. Not to mention that if values can be null, you have to know the details of any function you call to know if you need to worry about null values at all. See https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-computer-science/ https://www.lucidchart.com/techblog/2015/08/31/the-worst-mis... for more on how terrible null is. Likewise with exception systems, in large codebases you end up with try/catch statements all over the place, because it's hard to know if the functions you are calling will throw an exception, and even if you know they don't _now_, maybe someone will add a throw later. With explicit result types, you know if you need to handle errors or not, and that won't change unless the signature of the function changes. As for implicit type conversion, the example in the OP is obviously bad. But I don't think it sacrifices safety to allow implicit conversions for widening integer or floating point types, such as widening u32 to u64 or f32 to f64 (but not u32 to i32, i32 to u64, etc. I think u32 to i64 would be ok, but I'm not 100% sure).