
Dark side of ergonomics in Rust - Avi-D-coder
https://vorner.github.io/2018/04/08/Dark-side-of-ergonomics.html
======
hexane360
The 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?

~~~
ChrisSD
I think a more charitable read is that Vorner is urging caution. Ergonomic
changes are the "most prominent language work in the pipeline"[0] and there
does seem to be a general consensus that Rust is harder to learn and should be
made easier. The argument is that Rust should keep its priorities straight and
not be seduced by ease of use to the detriment of the language's advantages.

But I agree that the danger remains hypothetical unless we can point to some
dubious proposals that are gaining traction.

[0] [https://blog.rust-
lang.org/2018/03/12/roadmap.html#language-...](https://blog.rust-
lang.org/2018/03/12/roadmap.html#language-improvements)

~~~
hyperpape
I agree with your priorities, but I want to stress that don't think there's
any reason to believe the people working on the language are confused about
this. The ergonomics initiative has explicitly been about making certain
things easier without introducing ambiguity or jeopardizing the safety
guarantees Rust offers.

Like the OP says, the article is misguided, and if it has any effect, it will
create FUD in people who have not followed the ergonomics proposals enough to
know better.

~~~
phaylon
I was part of a couple ergonomics discussions and I disagree.

Yes, in the end the changes don't sacrifice long-term maintainability over
learnability/marketing, but that was a hard fight. And given editions will
allow radical changes to the language and ecosystem every 3 years, I'm not
sure how long that resistance is sustainable.

~~~
derefr
It seems to be that this is one of those fights where I (as a Rust developer)
would _prefer_ that both sides fight as hard as possible. An article like
this, by suggesting that the "ergonomists" move more toward the mindset of the
"safety people", could cause _undue_ wins of the "safety people" over the
ergonomists, when what should have happened is a careful compromise.

~~~
didibus
Can you really compromise on safety guarantees?

Isn't doing so just a rewrite of C++ ?

------
Wildgoose
I'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
I 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.

~~~
zzzcpan
Type conversions have to be explicit when they are lossy, otherwise it's just
useless noise and cognitive overhead that can lead to a bug.

~~~
Too
Type 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
Personal opinion, heavy use of int is a code smell.

Ada with 'range' probably gets this right.

------
Fronzie
The 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
Say 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.

~~~
heavenlyhash
> For exceptions to work predictably, all mutation within a try-catch block
> should be captured in a transaction that can be rolled back.

Wouldn't that just be beautiful?

~~~
the_grue
That sounds like a natural fit for Rust's lifetime-tracking mechanism. The
compiler can automatically drop all initialized objects upon exception. In
fact, I'm reasonably sure it does just that for panic handling.

~~~
panic
That example is initialization code, but you'd run into the same problem even
if the control were already initialized (and you were trying to set new
properties on it). In that case, you'd likely have a borrowed pointer, so it
wouldn't be dropped when the panic happened.

~~~
ordu
The pointer can be dropped. The pointed object cannot. So the problem of
rolling back the transaction changes ownership, now our button should do
sensible things in setLabel, to rollback changes made before an exception.

------
always_good
Speaking 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

      let 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?;

~~~
yjh0502
I used it frequently when mixing `Result` with `Future`. For example,

    
    
      fn do_something() -> Box<Future<Item=(), Error=Error>> {
        let val = match do_result() {
          Ok(x) => x,
          Err(e) => return Box::new(err(e.into())),
        };
        // do something with `val`
        unimplemented!();
      }

~~~
curun1r
If you want to cut down on lines, this is equivalent:

    
    
        let val = do_result().map_err(|e| Box::new(err(e.into())))?;

------
oblio
Is 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
There 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
You 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
The 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.

------
akulbe
I'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. :)

~~~
cesarb
Think 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.

------
thayne
I 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-
mis...](https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-
computer-science/) 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).

------
dom96
To 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.

~~~
fnord123
AIUI, 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
You could add some extra metadata descriptor to a function at a cost of space
and checks... (like some functional languages do)

~~~
pjmlp
Good 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
C++ 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-n...](https://stackoverflow.com/questions/46798456/handling-gccs-
noexcept-type-warning))

~~~
pjmlp
I doubt many would consider it metadata, specially since it goes away for all
non public symbols on release builds.

------
jononor
Are any of these actually proposed to go into Rust? Or something like it?

~~~
brson
I'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
I'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
I 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)?

------
kybernetikos
Now 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.

~~~
tinco
The 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
> 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
You 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.

------
snarfy
I 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.

~~~
phaylon
I'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.

------
millstone
> 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
From 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
The 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
Again, it makes the programmer aware of possible issues and gives them a nudge
to consider handling the error. This is very valuable.

------
ensiferum
Exceptions, errors and bugs are all different, have different semantics and
subsequebtly different outcomes. Also the author is confusing noexcept with
nothrow

~~~
topspin
>> 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."

------
zkomp
Hmm 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...

------
finchisko
I'm just reading it, but already like the disclaimer. :D

------
sanxiyn
Reddit discussion here:
[https://www.reddit.com/r/rust/comments/8asb4i/dark_side_of_e...](https://www.reddit.com/r/rust/comments/8asb4i/dark_side_of_ergonomics/)

