Rust beginner notes & questions

References around closures/iterators/auto-deref can be tricky. It takes a bit of experience to know iter() is by reference and iter().cloned() and into_iter() are the alternatives.

Rust doesn't have a concept of immutable memory. All memory can be mutable in principle (that's why you can always make owned objects mutable). Only references and variable bindings have mutability attached, and temporary immutability of memory is enforced by restricting where and when these references can exist.

The closest type would be &mut &T, but I don't recall such thing being used in practice. Maybe let mut x: &T is the thing you need?

2 Likes

I see people reimplementing identically named but incompatible traits as a failure: dual_num::Zero - Rust

I know that sounds harsh and nit-picky, but I feel that it's a sign of things to come. This kind of thing will inevitably get worse as a consequence of stripping low-level abstractions out of std. The missing wheels will be reinvented, and it's going to be a huge, incompatible mess.

(To clarify: I'm not generally advocating for std to include all-encompassing implementations for things, but I do feel that it needs to authoritatively and centrally define core traits and a decent set of data types, not just thin wrappers around whatever is provided by LLVM.)

Please take a quick look at the Diesel crate's cargo.toml dependencies.

This is a top-shelf library, but they were forced to depend on piddly little libraries for all sorts of basic stuff that absolutely does not belong outside of std. Numbers. Bit manipulation. GUIDs/UUIDs. Temp directory. Time. Urls.

Are you seriously saying that in 2018 it's okay for a new programming language just barely out of 1.0 to not include a UUID type!?! Seriously? They're everywhere: in file formats, protocols, APIs, and of course databases.

You have no idea how many "enterprise" databases I've seen with "nvarchar(36)" columns storing the text-representations of GUIDs for one reason, and one reason only: Java didn't originally include this type in their core library. Oops. Now there's thousands of databases out there with 74-byte primary keys because of that mistake.

My takeaway point is this: small mistakes or omissions in standard libraries and language design have an enormous multiplying factor in their cost to society. Thousands of developers write millions of applications with billions of users.

A small convenience for the language designer can cost literally billions of dollars down the road.

I'm aware I'm rambling a bit, but that's in part because I feel like I've hit a hundred little brick walls with code that I'm trying to write, and none of it has to do with the supposed "difficult learning curve" of Rust related to the borrow-checker. That, I understood just fine! :grin:

Instead, I got stuck on stupid little things, like basic loop syntax! Rust has a loop keyword, unlike most of its brethren. Okay, that's a bit odd, everyone else just uses while ( true ) { ... }, but okay, whatever. However, there's no do-while loop in the language! It took me way too long to realize that this is yet another unique and special Rust-ism, and the idiomatic equivalent is: while { ... body...; cond } {}

Meanwhile, all the loop types just desugar to loop & break anyway, so would it have killed anyone to just include do-while loops like every other language?

Okay, I might be rambling again, so to get back to your question: What's is my overall take on Rust, and what is stopping me?

I will poke around with it a bit just for fun because it has some interesting concepts, but as-is I don't see it ever becoming useful enough to warrant me using it for anything in production because it's headed in the wrong direction and fixing it would require breaking changes. Over time I expect Rust to get worse, not better. The little short-term conveniences like read_to_string are a slow death-sentence in the long run, unfortunately. The image in my mind is Artax slowly sinking into the quagmire.

Automatic dereferencing is just one example in this category. Someone got lazy and decided they didn't like typing asterixes all over the place and added an ill-thought layer of magic for their own convenience that is now a landmine for even the most trivial manual refactorings. Combined with the over-use of macros, I just don't see how the language will ever get IDE support equivalent to what Java had 15 years ago, let alone today.

If a gun was pointed to my head and I was forced to pick one thing that I could point to in Rust that I feel is a total failure and stops me using it is its strings. They are by default mutable, a concrete type instead of a trait, and there are way too many variants of it, not even including char arrays and the like that turn up in interop. I feel like this is a catastrophic design mistake. C++ got this wrong and Java and C# got it right by having one immutable string type.

(Note: I fully understand the practical need for separate UTF8 and UCS-16 underlying string representations, and I support Rust's choice of UTF8 by default.)

I particularly hate Rust's strings because 99% of the code I would want to write has to interop with Windows, C#, or Java APIs, all of which are OsString or some variant on a [u16] array, which is just too painful to work with because of all the small inconsistencies and missed opportunities for trait-based elegant code.

Take a look at this gem from String.rs:

impl_eq! { String, str }
impl_eq! { String, &'a str }
impl_eq! { Cow<'a, str>, str }
impl_eq! { Cow<'a, str>, &'b str }
impl_eq! { Cow<'a, str>, String }

First of all, that macro is local to that source file despite being completely generic. Clearly, implementing equality comparisons is a concept unique to strings. ಠ_ಠ

Would you care to make a bet as to whether OsString implements the same traits identically to String/str? Care to put money on whether OsString/OsStr interact symmetrically with String/str?

Let me save you the trouble:

let a: OsString = OsString::from( "foo" );
let b: &str = "bar";
if let Some(_) = a.partial_cmp( b ) { /* Compiles fine! */ };
if let Some(_) = b.partial_cmp( a ) { /* LOL, no. */ };

This kind of thing would rate a Wat!? in the infamous Destroy All Software talk.

Imagine for a second that you're a beginner and trying to compare an OsString to a static constant &str. Your code has a 50:50 chance of compiling depending on which order you use your variables, which would lead you to incorrectly assume that this feature is unavailable in the standard library about half the time.

That's crazy.

11 Likes

I take this as good feedback. We clearly have a user who is having a terrible experience with the language and is pissed about it. There maybe others having similar experiences but not being vocal about it. It needs serious looking into.

15 Likes

This may very well happen. The upside is that from the multiple implementations a portable and efficient version can be picked (or created with insight gleaned from the existing impls) and put into std. This boils down to: it’s always easier to add to std than remove/change.

As mentioned, the thinness of std is a contentious point - there are valid arguments on both sides. But look at Java - even it, despite a massive stdlib, has large auxiliary external libs that are widely used - the numerous Apache Commons, Guava, J2EE addons and so on.

Thin wrappers over LLVM? C’mon, let’s not exaggerate and detract from your points :slight_smile:

I agree that fairly fundamental APIs need to be available, one way or another. I don’t think it necessarily has to be in std however.

Well at least that’s good! :slight_smile:

There’ve been a couple of times a do-while loop would’ve been useful in my experience but it’s infrequent and a different formulation isn’t a problem.

As for loop, I like it - the alternatives of while(true) or for(;;) in other languages are just noise - you want a loop? Say it explicitly.

I suspect you’re referring to deref coercions rather than auto deref; the latter is completely useful and obviates lots of noise that would ensue otherwise. Deref coercions, however, can appear magical in the beginning, particularly when coupled with heavy type inference use. They’re less magical with some experience, and in fact a useful feature.

As for IDE support, we’ll see. It has already come a long way in just the last couple of years, and more work is happening. Java has had decades of work gone into its IDEs, and it’s a much simpler language to boot. But time will tell.

Both C# and Java have extensive use of StringBuilders, which is your mutable string. Java also has CharBuffer to view byte buffers as characters. Let’s not forget its StringBuffer legacy type either.

A Rust String is not mutable by default - the mutability isn’t tied to String itself; it has dynamic sizing facilities but it doesn’t mean it’s mutable all the time. String slices are great. The OsStr and CString/CStr are an acknowledgement of the different ways different things represent strings. Trying to shoehorn it all into a single type would likely lead to a bug laden path and a conceptual mess.

What trait do you want to see for a string?

Yeah, this is unfortunate. There are obviously ways to make this work (eg str has an AsRef<OsStr> that can then be used for equality). Not sure if it’s an omission that there’s no direct PartialEq or intentional.

4 Likes

This is what I imagine the story to be behind a lot of things that were stripped pre-1.0 like std::num::Zero.

Stabilization affects the language

At some point in time, the feature existed. However, there were things about it that were simply not as nice as it could be:

  • perhaps the best design was still unclear; it needed further experimentation and design iteration.
  • perhaps rust was missing critical language features to make it ergonomic (or these features existed and were buggy/unreliable), like const functions or higher ranked types. Zero appears to have been such a trait; its unstable feature message used to read:

    Unstable: unsure of placement, wants to use associated constants

  • if something was underutilized there could be unknown unknowns; poor design decisions lurking just beneath the surface that would only be discovered after stabilization. (and even with the team practicing their discretion, some of these still managed to slip through, like std::error::Error)

In the rush to 1.0, a decision had to be made:

Are we ready to support this thing in its current form, without breaking changes, for all of eternity? Or should we wait until it is possible to do better?

Some things (like the f64::consts modules) were simply so fundamentally important that something had to be stabilized even if it was a terrible hack! But many things were hidden behind feature flags on the notion that we can have something better.

For a while after the 1.0 release, you couldn't even sum an iterator on stable!! Things such as this were slowly added back based on how desparately they were needed. There wasn't a single person using rust who was not losing hair over the inability to sum an iterator, so std::iter::Sum eventually had to be added even despite being a terrible hack.

So that's my idea of how these features got removed. However, there is also a second chapter to this story.

The language affects the users

After the removal of something from the standard library, a paradigm shift can occur. People can go from thinking

this is basic functionality and it is appalling that I need to use a third-party crate

to beginning to think

that problem is so complicated that it doesn't even belong in the standard library. There is no design that is good enough to be worthwhile.

Maybe sometimes this really is true, but other times this view may simply by colored by the status quo. In a way, it's the blub effect, and I believe we are all susceptible to this.

I know I'm going to receive flak for saying this, but:

We are all at risk of adopting an apologist viewpoint on missing features.

...and it is at this point that the absence of something in the standard library may become self-sustaining.

How can this be prevented? I don't know. Identifying and challenging one's own cognitive biases is not easy. Sometimes somebody motivated appears and smacks whole the rust community with a harisen, writing an epic blog post to remind us all that yes, there is still hope for having such a feature in the standard library. But in general?

...hm, there I go again. Always seeing the problem, but never the solution...


I had an example of a possible personal case of this phenomenon, but it has gotten really long for this post. Maybe I'll make a separate thread...

23 Likes

I applaud the poster and responders here. I see this as healthy critical discussion. I don't interpret Peter bertoks comments as "pissed"; I see it more as intense, which I think is good that a program language should evoke intense feelings.
As someone who took to rust a few years ago, lost traction, and am now coming back to evaluate I very much appreciate discussions like these. When I evaluate a tech for adoption, which has impacts on the success of my business and my livelihood, I want to know what the warts are; downsides, issues; limitations of a language are far more important than features IMO.

Rust, unfortunately it seems, is now stuck with the "difficult" moniker that plagues Haskell; a sad psychology that creates learning anxiety in people before they even start. Studies on math anxiety have shown this is a real and lasting effect, causing measurable stress. Props to the rust community for taking "usability" head on.

As someone new to the community (and from the luxury of my vagabond status) I can report that I have had conversations with colleagues where they shared the sentiment that if both rust and c++ demand a high cognitive tax they'll just stick with c++. I find this unfortunate but both realistic and pragmatic. And by high cognitive tax I mean things commonly brought up about rust like fighting the borrow checker, type system, etc... , and things like inconsistent APIs (though I've got no real examples so maybe there is a large amount of FUD here...?).

I'm curious what are the pragmatic views/uses of rust? Id love to see examples a non-trivial AND pragmatic rust programs side by side with go and c++. Perhaps they are out there and I just need to look. I'd love to see rust advertised as a pragmatic solution to a class of problems. In my experience it would be a great balance to the well-known complexity. People who have never written or seen rust know it's complex and I'd like to pull out a pragmatic example and show them it's no worse than nodejs, go, etc.... IMO pragmatism trumps other inneficiencies and fringe quirks.

I'm still playing with rust and dedicated to learn as much as I can and I hope to find that it is the right tool to build my next project (fintech payment collections service using optical character recognition reading receipts and account summaries). To be frank, one reason for picking rust ( over go or c++) is the combination of generics, better guarantees for data races, memory, etc..., and a cleaner API and ecosystem (not really an issue with go).
I have a high tolerance for learning new things and am not afraid to pay people to do the same, if it means landing on superior tech. Part of superior tech is productivity, which I define relative to project and cost of failure. I'd rather spend an extra month(s) building something if that means it will require less maintenance, bugs, and errors over the course of time. I see rust as being able to provide that kind of productivity (versus deliver fast and spend months debugging). But I get concerned when people I know, who are polyglots and not averse to new things, eliminate rust due to complexity... And I can't help but wonder if that complexity is related to Peter bertoks general issues.

Thanks for all the work you do. I hope I'm not coming off too negative or as trying to create unnecessary anxiety or as telling experts how to do their job.

8 Likes

I'm not sure if this counts as a "pragmatic" program, but one of the things that won me over to total Rust fanboy status was when, only a few weeks after reading The Book for the first time, I looked at the implementation of std::arc::Arc... and I could understand it. Easily.

This is in contrast to my complete inability to read just about anything in a typical C++ std implementation, despite multiple years of experience being paid to write C++ code and reading plenty of books on C++ obscurities. I know by heart how a typical std::shared_ptr is laid out in memory, but the actual code of any non-toy implementation remains total gibberish.

This is unfortunate. However, the good news is that it's not a universal view. There are also plenty of anecdotes from people with a background in scripting languages that found Rust "made systems programming accessible" for them.

You may have seen the quote (does it qualify as a meme yet?) that "Rust is difficult because writing correct code is difficult". I don't like to say it too often because it risks coming off as shutting down legitimate critical discussion, but there's also a lot of truth to it. Most of the things that are hard in both Rust and C++ are part of the essential complexity of systems programming, like understanding ownership, stack vs heap memory, race conditions (as opposed to data races), etc. So "the sentiment that if both rust and c++ demand a high cognitive tax" is technically correct... but it also ignores the many, MANY sources of accidental complexity that C++ mostly inadvertently created and Rust, with the benefit of hindsight, successfully avoids or actively defends against.

10 Likes

I stepped into Rust by starting to rewrite a command line app which I had written previously in Perl.

Although the Rust learning cure is pretty steep (but manageable for my background) I'm very pleased with

  • the expressibility the language offers (comparing here to e.g. Go is an insult to Rust)
  • the speed programs run (especialy in comparison to many other languages)
  • the Rust Programming Language Forum here which is extremely helpful (quick and high quality answers)
  • the documentation which is very good (only to mention the Rust book)
  • the compiler which is very helpful in preventing me to do stupid things on the one hand and to suggest viable solutions on the other hand
  • the availability of Rust books (the good point is: if anybody is short on money there is the very good Rust book), to name a few: S. Klabnik's new book, J. Blandy & J. Orendorff's book
  • the availability of blogs aso which are really good (ok, one has to check how old they are... as the language has evolved over time)

Of course, it doesn't mean that there are no situations where I'm struggling to understand things but in the end I'm confused on a higher level of consciousness (which is not bad for my understanding of life. and progress..). :slight_smile:

8 Likes

haha, totally agree.

to me, this is what peter_bertok is trying get at, and that in his view rust is already starting to slip into some bad habits that led to complicated c++, the latter of which has had many years to accumulate such cruft. Perhaps that just the nature of an aging programming language that has to evolve with other advances (we don't really know since this is all pretty new to us as humans, maybe it'll be normal in a few hundred years that programming languages just need to die :woman_shrugging: )

I tend to think about things in terms of "sophistication == active refining simplicity" and "complication == active emerging conflict"... and "simplicity" as "achieving a goal with least resistance". I think here the goal is for Rust to be sophisticated without being complicated. writing code is really a struggle between sophistication and complication, and its never ending.

2 Likes

My point is that these bad design decisions were entirely predictable, and could have been avoided with a tiny bit of experience and foresight. It's just a matter of inspecting the history of other languages.

In my experience, most popular languages go through a life-cycle that goes something like:

  1. No templates, trivial APIs. Everybody is impressed at how lightweight and simple everything is. There are many convenience wrappers around OS concepts such as the POSIX or Win32 file descriptors wrapped in a convenient Stream class, hiding some of the 1960s legacy behind a thin facade. A half-arsed attempt at i18n is grudgingly included, but there is clearly an anglo-centric feel to the language. That's okay, all the initial developers are in the western world, so you can get away with this. This is 1990s C++, C# 1.0, Java 1.0, and Go currently.
  2. That template itch just won't go away, so it is hacked into the language. A lot of APIs are duplicated, such as IEnumerable vs IEnumerable<T> in C#. Legacy APIs like Stream are left byte-only, because it's just too hard to fix it now. This is C# 2.0, Java SE5, early 2000s C++, etc...
  3. There's a grudging acceptance that the rest of the world will refuse to learn English, so the i18n APIs are aggressively expanded. Now there's two versions of the String APIs, one with the old defaults and one with the new comparison options. There's usually several of each of of the date, time, and calendar types now, because the real world is complicated. Sometimes things just get shredded because an intermediate library hasn't been updated to handle DateTimeOffset or whatever. Bad luck!
  4. Turns out templates are hard! They need all sorts of restrictions such as being constrained to value (copy) types, types that implement a particular API, and so forth. The C++ guys are still trying to work this out. Rust has had this since before v1.0 via Traits. <- You are here
  5. At some point someone realises that with the advances made in step #4, a lot of APIs can be rewritten to be vastly more elegant. "Obviously", making things like Stream a lightweight synchronous wrapper around a byte-only file descriptor was a mistake, so now the entire language is slowly reinvented piece by piece, leaving an absolute mess of incompatible APIs next to a bunch of legacy garbage. Modern C++, dotnet core 2.x, and Java 10 are here now.

First, please read this article because it spectacularly illustrates my point: Pipelines - a guided tour of the new IO API in .NET

The dotnet core guys came up with this, and it's great, but now 99% of the code out there is based on the old stuff, so this won't be used much. Third-party libraries and large enterprise systems will continue to use Stream, directly or indirectly. For example, XmlReader will probably never get properly rewritten in terms of the Pipe API, because it would be a breaking change to make proper use of the efficiencies, such as allowing consumers to use Span<char> instead of heap-allocated String instances. It's too late. The language was built up incrementally, and you'd have to make a new -- incompatible -- version to really make use of the features. (Just look at Python 2 to 3 or Perl 5 to 6 to see how easy that is!)

We know what a stream API should look like in any language that's gone past stage #4. My point is that Rust essentially started at stage #4, its developers had all of this history to reference, yet std:io::Read looks very much like the C# 1.0 Stream API that is finally getting replaced. It inherently makes copies, it is inherently byte-based, and it mixes in unrelated string APIs that prevent a backwards-compatible upgrade to a template-based version. Etc, etc...

In fact, as an API the Rust version is objectively worse, to the point that I can 100% guarantee you that it must be eventually thrown out and replaced by something better thought out.

For example, the Rust Read trait forces UTF-8 on you, so even if you just want to read UCS-16 out of a binary stream, then you... have to go down a completely different API path! Err... wat? Even C# 1.0, back in 2002, got this right! It has a separate TextReader classes to wrap byte streams in a specifiable encoding. The underlying Stream class makes no such i18n assumptions. Remember... not everybody speaks English and not everything is UTF-8, no matter how hard we want this to be true!

This is what disappoints me about Rust. It got a running start compared to other languages, it was developed with decades of history to reference, yet it seems to insist on repeating the same mistakes...

PS: I'm not the only one with this point of view: https://www.reddit.com/r/programming/comments/8vjjgu/pipelines_a_guided_tour_of_the_new_io_api_in_net/e1ow1an/

PPS: I take it all back, I just had a play with the C# Pipelines API, and it turns out that it is not template based, its data stream is always made up of bytes. I was tricked by seeing SomeClass<byte> in code samples, but that's just code reuse. The API itself does not generalise to other value types such as char or whatever. Sigh...

6 Likes

A more accurate moniker is "difficult for beginners". Once you know the language, you are as fluid in it as any other (even more so in comparison with some like C++ where you have to stop and look over your shoulder every now and then).

2 Likes

Note that I consider this a far lower "cognitive tax" than having to run the borrow checker entirely in my own brain like I do in C++. As a result I'll at least try borrowing things (and .clone() if it gets awkward) in Rust where in C++ I'd just copy-construct from the get-go since it's so hard to be sure I didn't break something.

It's certainly true that designing in a way that best leverages the checks is something that needs to be learned, and that some things (certain kinds of graphs seem to be the usual example) just aren't an elegant fit for the model.

I'd be great to hear exactly what they were from them. This thread has a ton of things in it, from tiny to philosophical, so I suspect their list would be somewhat different.

1 Like

Some things can't be changed now, so there's no point in distressing about them. Some other things can be fixed or added with a deprecation. So my suggestion is to write down a very long list of the things you don't like, remove the ones you believe are impossible changes, and open a separate RFC for each of them for the Rust 2018 edition (or where possible even for Rust 2015). And then be humble when people tell you some practical problems. Most ideas will be closed or shot down, but if even very few see the light in Rust 2018 you will have a positive and multiplicative impact on future Rust users :slight_smile:

10 Likes

I’m curious if you’ve been able to get over your std::io::Read gripe and look at Rust some more, beyond your initial post in this thread.

I also think it’s incredibly unrealistic to expect a language and/or its stdlib to be flawless, regardless of how many have come before. Doubly so if you actually intend for people to use it rather than sit in someone’s imagination.

2 Likes

Perhaps you could start an RFC and we could all iterate on it to create a "proper" Stream API for Rust? I think you've pointed out a lot of really good ideas and points that should be addressed. I pretty much agree with your analysis, though, I would not currently have had the foresight to so succinctly categorize the issues.

1 Like

Rust has many warts. Its feature-set does appear less cohesive and consistent when compared to some languages (C# comes to mind). There are countless other problems mainly due to its youth. Then there's the borrow checker. So feeling negative emotions is almost a rite of passage for a Rust beginner. I don't want to belittle your feelings, but in my experience once you get past this initial despair and when you get to coding for production instead of doing toy programs is when you come to appreciate what a life saver and how brilliant this language is. This is because Rust's strengths which far outweigh its faults unfortunately become apparent only when you've done any real-world work in it. And that'll perhaps forever be Rust's curse.

8 Likes

C# is a very high bar, it's one of the best designed languages out there (still, I think Rust is better for my usages).

3 Likes

I love Rust - let me preface with that.

Unfortunately, I think the tears at the seams (i.e. seeming incohesion/inconsistency) are visible at both the early/beginner stage and also at a later stage, although they're for different reasons. I am very hopeful that, over time, they'll be ironed out to a point where they're barely noticeable.

That said, no language is perfect. Rust is doing something novel, certainly so for any language that can be called mainstream. I think it's understandable that it'll have some growing pains, both at the lang and stdlib levels. There's just no way around it. I think the communities' (and Rust core teams') priorities align very well with mine (i.e. robustness, expressiveness, correctness to borderline pedantry, and performance).

Rust will definitely not be for everyone, just like no other language is universally praised or liked. The people that will like it are the ones holding the same core values as Rust and willing to put in the time to learn it, with all its quirks and idiosyncrasies.

4 Likes

Absolutely. I would the say the same about C#. That said, one thing that can be mentioned in Rust's defense is that C# has GC which makes a lot of decisions easier. We only have to look at the state of the art prior to Rust when it comes to non-GC languages to appreciate that Rust is a huge improvement. That said (recursively), not all of Rust's difficulty or unsightly parts stem from the memory-management challenge it's set for it itself.

4 Likes