Rust language and special cases (blog post)

Saw this blog linked on reddit: The Rust Language and Special Cases — Infinite Negative Utility

What are people’s thoughts on this, as users of the language? Not specifically the try fn feature used there, but the greater point of the blog.

I have to admit that I harbor some of those feelings as well, particularly this year as some hotly debated features were introduced. Curious what other members of the community think.

23 Likes

I could not agree more with this post. You've The author (Getty Ritter???) put my own thoughts to words much better than I could have done myself!

6 Likes

Just to be clear - I’m not the author of the blog :slight_smile:

2 Likes

Oops! Thanks for correcting me.

The author of the blog post names orthogonality as a property of well-designed programming language features. I could not agree more with this assessment, as it's a conclusion I've drawn on my own several years ago.

Specifically what orthogonality allows is clean composition of source code, similarly to how pure functions can be composed to create more complex functions. When a language feature is not orthogonal, this introduces weird corner cases to the language that makes using that feature in complex contexts a pain to use (the simple use cases are never really a problem).

Therefore I think orthogonality is a useful (qualitative) metric in judging new language features.
Not a goal per se, but it's definitely useful to keep track of it.

12 Likes

It reduces a complex multidimensional trade-off to an attractive 1-dimensional oversimplification. Orthogonality/only-one-true-way-to-do-things is good most of the time, but it's competing with other concerns, such as terseness, expressiveness, readability and ease of learning and using the language.

Brainfuck is as simple and orthogonal as a language can be, and can be learned in one minute. And yet, it isn't ideal, so there are other concerns :slight_smile:

Rust used to have only match, but it was annoying to write full match arms for every wrapped value, so it got redundant if let, then try! and then went all the way to ?. If orthogonality was more important than anything else, Rust would look more like MIR, with only UFCS and no for loops.

Advanced users demand special-case syntax for the most frequent operations. Another way of looking at it, is that languages evolve towards minimizing Kolmogorov complexity of programs in their domain.

12 Likes

As a real novice in programming coming to Rust from a self-taught background, I have had the common experience of finding Rust difficult to learn. I have little to offer on the generalized concept of language design and the competing concerns involved therein.

That said, having wrapped my head around algebraic data types, fostered in part by a detour into Elm, I think obscuring the specific case of Result<T,E> would retard the fundamental learning process in the end. The second footnote shows how readability can be achieved in the current language without making fundamental design changes. I would suggest for readability purposes, maintaining parallel structure for both arms of the conditional an option would be to introduce an ok! macro, thus turning the example into

fn sample(lst: Vec<T>) -> Result<(), String> {
    for x in lst {
        if is_this() {
            return ok!;
        } else if is_that() {
            throw!("Bad!");
        }
    }
    ok!
}
5 Likes

Let me attempt to write down my personal thoughts, perhaps bordering on rambling. Keep in mind that these are my opinions only - I don’t pretend to think they’re correct in the absolute. Language design invariably carries a lot of style and taste to it.

First, I find the focus on “beginner friendliness” a bit premature. Rust hasn’t been around long enough to have sufficient in the field experience to truly assess what helps vs what’s just noise. I found some things harder to understand than necessary when learning, but once I found a way to learn it, I didn’t think the feature was actually difficult or incohesive or malspecified. Sure, there are certain features that fall off a cliff at the edges, but most of those have some plans to be corrected.

The issue with learnability is multifaceted, but I’d say the biggest gap was documentation. Even to this day there aren’t many Rust books that really explore the language in depth, or at least offer a deep dive in addition to intro level coverage. We’re getting there and some of the more recent books show promise. So some of the learning curve can be flattened by “simply” having better learning resources. But we, as a community, need more time with the language with improved docs to really get a feel for what’s still rough around the edges despite the docs.

Consider also the difference between NLL and something like impl Trait in the argument position. Before I go on, I really don’t want to litigate specific features in depth again, but I need to use something here to illustrate my point.

NLL makes code more beginner friendly, but it also fixes an impl and design shortcoming of the existing borrowck. There are workarounds for some cases, and it trips up beginners, but this is a feature that both beginners and experts will welcome because it fundamentally fixes something that’s at the cornerstone of the language. This is a great example of a feature that needs all the focus and energy of the community.

impl Trait in arg position doesn’t do that. I’m not going to say more on it as I don’t want to reopen that can of worms. I think most of you can see the stark difference between the two, even though they both overlap in the “beginner friendly” territory. Throw match ergonomics in here too while we’re at it :slight_smile:

Now, I realize Rust has garnered sufficient community involvement such that multiple features are discussed, prototyped, debated, and occasionally implemented. That’s great! At issue are distractions. For all the work on fundamental features, there’s a lot of distraction of people working on those features with, how shall I put it, less fundamental ones. We should put a moratorium on such changes until the fundamental stuff is in place, we have better learning resources, and more mileage under our collective belt.

Second, there should be just as much, if not more, focus on improving things from the perspective of intermediate to advanced users. Reason? You’re a beginner only once but stay at the further levels much longer.

Third, and probably the summary of my feelings, the focus should be on features that actually enable new things, either with significant reduction in how they’re emulated today or increase performance or better compile time checking, etc. Some examples of this would be GATs, const generics, coherence improvements, HRTB improvements (or bug fixes), more const eval capabilities, custom allocators, self referential borrow capabilities, inline asm, async/await, abstract types, etc. These things, in a systems language like Rust, move the language forward. They’re hard topics, and require a lot of focus and energy. We shouldn’t punt on the other stuff, but the bar for them should be higher than normal right now; the bar for changes should be high, period, but there should be extra level of scrutiny for the time being. Marginal features or unconvincing ergonomic improvements can always be added later, at a cheaper cost than removing them, and with more collective experience to lean on if they still scratch a sufficient itch.

Inevitably, mistakes will be made - that’s fine and expected, particularly in a complex and novel language like Rust. I think epochs are a good strategy for eradicating or fixing them from time to time. My point mostly is about where we focus and steer the ship right now.

22 Likes

I find myself fully agreeing with the points made in the blog.

I find myself in agreement with many of the things that you say here. But you know, poor old Beelzebub needs a lawyer. So here is my attempt at representing an alternate point of view.

If your only objectives when building a programming languages are usability for experienced users and advanced static type system capabilities, then you get a programming language like Haskell, and I think it is important to realize that while admirable in many ways, this role model is not without its issues.

Haskell is a successful programming language. Its type system lets you do crazy things that are checked at compile time, and it manages to take keyword noise and redundance to a ridiculously low level. But any time I tried to learn it personally, I got stuck in front of the wall of sigils, impenetrable user jargon, and underlying assumption of much documentation that I knew category theory or cared enough about it to face the complexity wall of learning it before I can be comfortable with this programming language. So if Rust went in that direction, it would be without people like me or a similar background.

As a language designer, it takes conscious effort, and a careful attention to detail and newcomer experience, in order to avoid falling down this path. When you're used to it enough, and know how to avoid the deadly traps so well that you don't even think about them anymore, even a mess like C++ can seem reasonable. Worse yet, many beginner complaints will never be expressed publicly. It takes a fight against one's ego to dare ridicule yourself by asking a question on something "which must be obvious" on the forums, so some people will just silently get used to the roadblock and either learn to get around it or give up, just like inexperienced Windows users will over time get extremely efficient at dismissing recurrent incomprehensible error messages on startup.

This is why, in my opinion, it is good for programming language designers to also think about the papercuts.

Concerning this, your other point is that now is not the right time to think about them. That we have more important things to take care of first. That we should be working on the plumbing before we can paint the top of the house.

I would agree with that if Rust was still publicly presented as an experimental language with a big "DO NOT TRESPASS UNLESS YOU ARE READY FOR THE WORST" sign at the entrance. But it's not. Rightly or wrongly, some of us have decided that the language was already in a good enough shape for some use cases, and that the time was right to market it as a stable platform that one can start building stuff on. Now that we do this, we get plenty of curious beginners. And as soon as we have beginners, we need to care about them, which you personally contribute admirably to every day.

One of Rust's crazy gambles is to make advanced type system features easy to get started with as a beginner. It manages to hide the complexity incredibly well, and to let one use the language without fully understanding what's going on at first, then gradually acquire a more advanced understanding over time. I think that's one very precious property to keep as the language evolves. We should not abuse this because correcting an incorrect understanding during the learning process is always painful, but it's good that we can afford to introduce some pretty crazy stuff down in the type system without scaring away most of the beginners like Haskell does.

Because for every user that gets stuck in the learning process and goes away, we get public complaints about how Rust is impossible to learn, and unworthy of attention. We get companies that run internal evaluation of the language out of the curiosity, and get away from that with a feeling of "not worth learning it". Rust is toying with the boundary of this all the time, and it's important to realize how fragile our position is, how big the gamble was.

In that sense, now is the right time to care about beginners. As a young language which just went past stabilization, Rust is now at a critical stage of its evolution where it needs to convince the programming community that it is worth learning and building stuff with. From this point of view, beginner experience matters in many ways now a lot more that it will in the future, once things are more established, we have many production users, and we are at a lesser risk of short-term extinction.

From this point of view, I can see the point of, say, match ergonomics. Match expressions are one of the first things that one learns as a Rust beginner. But by forcing you to explicitly destructure everything, they break a bit this "illusion of simplicity" that things such as method syntax or deref coercions managed to build. From this, I see two paths : either we consider pattern matching as an advanced topic, and we teach it much later in the language learning process, or we make it more approachable to newcomers by allowing them to stick with some illusions of understanding for a little bit longer. So far, we took the later option.

I also disagree with you that NLL is that different. One of the ideas behind lexical lifetimes was that the lifetime mechanism should be easy to understand correctly, by reusing the intuitive understanding which one has built from Drop. NLL builds on the observation that making this feature easy to understand correctly was not worth the trade-off, caused too much pain, and that it was better to go towards a mechanism where it is easier to get away with a wrong intuition because the language gets closer to "do what I mean".

To conclude, feature prioritization is a difficult problem, but I think that beginner-oriented features also have their place at this stage of Rust's evolution. I hope I have done a decent job at defending this idea, in spite of having little time available to write and correct this post (which is likely full of typos, rambling and redundance)...

...even though personally, the things that I'm most interested in now that I've went past that learning stage are const generics and SIMD :slight_smile:

13 Likes

@HadrienG, thanks for the thoughtful rebuttal/response! I don’t have time for a more elaborate reply at the moment, but a couple of quick things.

So just to be clear, I wasn’t suggesting it be built solely for experienced users. I said it should keep expert users in mind just as much if not more than beginners. In addition, I think beginners can be helped tremendously at this point in Rust’s lifetime by better docs.

Lexical lifetimes don’t make things intuitive. It’s actually the other way - you feel that you finally know how lifetimes work, and then you hit a LL limitation that makes zero sense semantically. That’s the opposite of easy understanding and intuitive. It’s an edge case! It also bothers expert users, although they know about it and try to find workarounds.

Match ergonomics and impl Trait in arg don’t fix edge cases. The former tries to add more sugar and the latter ... I don’t know what. But we can see many people against match ergonomics and impl Trait in arg position. I don’t know a single person who’s against NLL! :slight_smile:

7 Likes

If orthogonality was more important than anything else

I'm not saying it is, rather that it is worthwhile to add it to the "standard list of dimensions" along which every new feature is judged. If we keep all else equal, orthogonality is a desirable property for a language feature to have :slight_smile:

8 Likes

The difference is that if let, try! and ? are locally evident, contrary to try fn as empathized in the blog post.

9 Likes

Agree.

1 Like

Needless to say, I wholeheartedly agree with the concerns of the author. I don't know what happened to the community and/or the core teams of the language during the past few years, but it's a worrying pattern to see emerge.

Since a lot of feedback was given recently from Rust users who feel similarly, I still hope that this will be resolved somehow. If the leaders of the community and decision makers hear us, that is.

1 Like

I dislike this framing more and more. Many of these proposals are put forward from the community and many of these proposals have been criticized by team members and community members. I'm heavily critic of many of the solutions brought forward for the Ok(()) problem.

See, as an aside, the module debates, last year: it failed 3 times. The current module system is a mess of special cases coming out of pretty straight-forward rules. The new one will be a vast improval, because of all the discussion. The Rust language has it's fair share of problematic things already and they are improved by actually revisiting, not bolting on.

The blog post makes it seem though as if trailing Ok(()) isn't a problem. As someone who trains Rust professionally: it's a major source of confusion. The blog post makes a decent effort at highlighting the problem of many solutions: they add more rules to learn.

The last line of a function in Rust is terribly special, for example ? has unintuitive behaviour there (leading to things like Ok(some_falliable_action()?) or some_falliable_action().map_err(|e| e.into()). This oddness comes out of orthogonal rules, that interact in a bad fashion at this place. Given that we do have Try, it makes perfect sense to use it here. A good how hasn't been figured out yet, hence the plastering with failed proposals.

try fns are, IMHO, the currently best proposal for this issue. It might not be the last. The road to it is riddled with previous RFCs and pre-RFCs, yet people frame things as "worrying pattern". RFCs in this area have a tendency to fail rather then succeed.

Here's two other examples of failed ergonomics RFCs, just for reference:

Framing this as a "team vs. users" thing is an annoying pattern I see more and more and given the work we invest in communication, it's incredibly harmful :/. We're one of the most communicative FOSS projects and there's no intend to change that but hearing people argue a divide in this fashion really, really hurts.

15 Likes

Because it isn't – however, it's seen as a minor inconvenience by some. It doesn't warrant making a new and very narrow special case, though.

How so?

Why is that considered unintuitive or weird or problematic, though?

It's not team vs. users – it's users and team members who want radical changes vs. users and team members who don't.

5 Likes

To be clear: It is not try fn by itself that I view as a problematic pattern. It is the trend of try fn and other suggested features like it, which have in common that they:

  1. Add more surface syntax to learn. One such feature is not a big deal, but 10 are, especially when those features need to interact somehow.

  2. They do not add capabilities to Rust that it did not have before. This is arguably the bigger issue: if a larger and larger share of new features don't even increase Rust's capabilities, then what are they good for?

In the case of try fn I can add a third: It is not orthogonal, while the features it is built on are. Specifically, try fn is coined from: functions, type usage (as in Result) and some magic syntax + backing semantics. The combination is not orthogonal as it is not straightforward what would happen when combining it with e.g. async/await i.e. it doesn't compose cleanly when using it as a primitive. That single issue can of course be solved, but now you have a complex feature in the language where orthogonality won't help you bite it off as a separate chunk to understand it either.

FWIW: I have serious trouble believing that someone who has trouble understanding a trailing Ok(()) expression in a fn and the reasons for it will somehow understand what's going on with a try fn. The reason is that in order to do that you need to unwrap all of try fns magick mentally anyway.

6 Likes

I realize this is subjective, but this really isn’t a problem. And even if we agreed it’s a problem it would be at the absolute bottom of list of things that need improvement. And that’s what I meant about distraction - debating bottom of the barrel things at this point is counterproductive to the community at large.

We need to make changes that drive further adoption, by production users. There’s not going to be a soul that drops the language because of Ok(()).

Again, let’s focus on the things that matter right now and shelve the insignificant controversial stuff for later, when the foundation is rock solid.

6 Likes

This sums up my feelings on this as a "beginner-friendly" feature. Making people think they understand something that they do not is the opposite of teaching them. I'm not saying ergonomics aren't important - or that the way things currently are is perfect - but you can't make things simpler by obscuring how they actually work.

Moreover, you can't make things simpler by making them more complicated - which is what is likely to happen when you add multiple new syntax items.

5 Likes