Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login

> You can use rust as if it were a high level language without knowing anything about low level stuff.

How can you write that with a (I suppose) straight face? Rust makes you think about lifetime of every single variable, as anyone who's written a couple of lines of Rust knows... it' not just the lifetime annotations you will need when you get past the "copy everything" phase and start using or writing data structures, but the borrow checker making even the simplest stuff something you actually have to think through carefully (do you need to pass a reference, make a copy, use a mutable variation of some function, open a new block to limit the scope of the variable, assign it to a local variable to avoid it getting out of scope too early?). This is plainly "low level" stuff you need to worry about all the time which you just don't in any high level language.



sort by: page size:

> You essentially can’t write rust at all until you understand rust references, lifetimes (implicit and explicit) and the borrowchecker.

You can if you're willing to use stuff like .clone() and the interior mutability types. In Rust, you can tell when code has been written to be a bit sloppy because it has that kind of boilerplate. And the compiler checks are a huge help when it comes to refactoring the code and making it cleaner and better-performing.


> And who likes doing this to themselves anyway? Isn't it a very frustrating experience? How is this the most loved language?

The thing is, these dependencies do exist no matter what language you use if they stem from an underlying concept. In that case rust just makes you explicitly write them which is a good thing since in C++ all these dependencies would be more or less implicit and everytime somebody edits the code he needs to think all these cases through and get a mental model (if he sees it at all!). In Rust you at least have the lifetime annotations which make it A: obvious there is some special dependency going on and B: show the explicit lifetimes etc.

So what I'm saying, you need to put in this work no matter which language you choose, writing it down is then not a big problem anymore. If you don't think about these rules your program will probably work most of the time but only most of the time, and that can be very bad for certain scenarios.


>Anyone writing these things today in C or C++ already understands object lifetimes and Rust just adds a static checker for them.

As someone who only used Rust casually - understanding object lifetimes and knowing how to encode this in Rust type system is not the same thing. Not to mention that Rust can't statically prove some things that are valid (eg. cyclic references).


> It was a weekend learning project, and all I wanted to do was create a freshman level data structure from scratch.

It's worth mentioning that the implementation difficulty of data structures in Rust is not representative of the general difficulty of programming in Rust. Just because this is hard doesn't make it a bad language. Rust's philosophy is that you deal with the trouble of writing unsafe code each time you need a new low level abstraction, and when done, you don't have to worry about it again.

Also, writing generic datastructures in C++ without falling afoul of UB, and/or getting destruction semantics right can be quite tricky. Rust ... really doesn't have additional issues here; raw pointers in Rust work the same way as in C++, except they're a tad verbose. The only difference is that in Rust you probably want to provide a safe API, whereas in "freshman C++ datastructures" the datastructure does not have the unsafety neatly encapsulated. This is a hard task in both Rust and C++, especially with generic datastructures.

Writing datastructures isn't really a common task. It's a "simple" task because it's simple in C and C++, but here's no real reason it has to be in Rust. You can write inefficient datastructures in 100% safe Rust without too many problems, much like you can implement datastructures in regular-joe Java. Writing efficient, low-level datastructures can be pretty hard, but like I said it's a rare task so not much of an issue.

http://cglab.ca/~abeinges/blah/too-many-lists/book/ and https://doc.rust-lang.org/nomicon/ are pretty good resources about writing unsafe Rust. The exact boundaries and semantics of unsafe code are still being pinned down so while I do want to improve this documentation I'm waiting for that to happen first.

In general I advise folks to not write unsafe Rust till they are sure that they have a clear grasp of safe Rust. But if you want I can have a look at what you tried and help you improve your design. I'm very interested in making such things more accessible (on one hand, the Rust community doesn't really want to encourage the use of unsafe code, on the other hand, sometimes you need it and it would be nice if it was easier for people to use when that situation arises) so learning what people stumble on is important to me.

> Unfortunately, I don't think they're very concerned about my particular use cases (and maybe they should > > I think all this says is that you don't write very much of the kind of code which benefits from generics. You can dismiss my point of view, but some of us do use them a lot, and it's one of the driving reasons I use C++.

It seems like you're using generics the way you use templates for metaprogramming in C++. I totally get how powerful TMP is (I love to use it myself, though I can get carried away), but be aware that Rust is a different language and you have to approach it differently. It's important to consider the limitations of the system when designing a solution -- if I designed a solution for C++ and implemented it in Rust, I'd hit problems in the last mile and find it very annoying to make it work (using macros or something to fill in the gap). However, if you design the code from the start with Rust's advantages and limitations in mind; you may come up with a different but just as good system. This is something I hit every time I learn a new language. I hit it with Rust, Go, and many years ago, C++. So it may help to approach the language with a fresh mind.

Yes, coherence is painful. I seem to hit coherence issues pretty rarely myself, but they're annoying when they happen. There's work going on to improve these pain points (specialization!), but it's overall not that big a problem.

I think an example of what you tried might help. Preferably a more holistic example, condensed minimal examples tend to hide instances of the XY problem.

> Disclosure: I don't really know what an "overlapping instance" means in this context.

cases where you have more than one implementation that make sense for a given call. C++ solves this by allowing things to overlap and introducing overload resolution rules. Rust solves this by not allowing overlap.

> but I'm guessing the memory foot print would be at least twice what it is in C++. (I should measure that though.)

Go does make extensive use of the stack and has decent escape analysis, so it might not be that bad, really. But measuring it is probably the best way to go here.

> Some other time it's because the standard library doesn't have a trait for a commonly implemented method, so I can't write a generic function to call that method.

Do you have examples? In some cases generic methods are outside the stdlib in different crates, e.g. num_traits.


> there is a real cost to having to manage object lifetimes in Rust.

I disagree. For the most part, you don't have to worry much about lifetimes in Rust once you have programmed in the language for a bit. It's a learning curve, like any other (functional languages have their own learning curve). There are cases now and then when you do have to think about it, but mostly it's either quickly fixing mistakes caught by the compiler (like any other type error, and this reduces as time goes by), or writing the correct code from the get-go.

That doesn't make Rust a better choice than the functional languages, but it certainly doesn't make it worse.

(of course, there are other reasons as to why Haskell/etc might be a better option for these people)


> we don’t know how to create a simpler memory safe low-level language.

This may well be true, but using a memory-safe language is never, ever the goal. The goal is creating correct and secure programs -- that have as few bugs and security flaws as possible/required -- as cheaply as possible. While a memory safe language in the style of Rust is one means toward that end, that eliminates an important class of bugs at the cost of language complexity, it is not the best way toward that goal, at least not that we know, and it is certainly not the only one [1]. I.e. the hypothesis that if I want to write a program that's as correct as possible/needed as cheaply as possible then I should necessarily use the language that gives me the most sound guarantees regardless of the cost this entails is just some people's guess. It's hard to say if it's a good guess or a bad one because it's clear that we're talking about specific sweet-spots on a wide spectrum of options that could be very context-dependent, but it's still a guess, with good arguments both in its favour as well as against.

> In Rust, there are choices to be made, some important enough to have dedicated syntax.

Not only that, but those choices are exposed in the type signature and are, therefore, viral. Changing some internal technical implementation detail can require changes in all consumers. This is not a problem with the type system -- on the contrary, Rust's type system ensures that all the changes that need to be made are made -- but it is a fundamental problem with all low level language. They all suffer from low abstraction, i.e. a certain interface can represent a smaller number of implementations than in high-level languages (even if the choice is not explicit in the type, like, say, in C, the usage pattern is part of the interface). But Rust's choice to expose such details in the types has its downsides as well.

> If you use C, you can use formal methods to prove the absence of undefined behaviors

C now also has sound static analysis tools [2] that guarantee no undefined behaviour with a nearly fully automatic proof that scales to virtually any code size and requires relatively little effort, certainly compared to a rewrite.

[1]: Another low-level language with an emphasis on the same goal of correctness, Zig, takes an approach that is radically different from Rust's and is so simple it can be fully learned in a day or two. Which of the two approaches, if any, is better for correctness can only be answered empirically.

[2]: Like https://trust-in-soft.com/, from the makers of Frama-C


> To be honest, everything in Rust looks a bit ugly to me.

I write a lot of Rust code at work, and I admit that it can sometimes be pretty noisy. There are several major contributors to this:

1. Rust offers fine-grained control over pass-by-value, pass-by-reference, and pass-by-mutable reference. This is great for performance. But it also adds a lot of "&" and "&mut" and "x.to_owned()" clutter everywhere.

2. Rust provides support for generics (aka parameterized types). Once again, this is great for performance, and it also allows better compile-time error detection. But again, you wind up adding a lot of "<T>" and "where T:" clutter everywhere.

3. Usually, Rust can automatically infer lifetimes. But every once in a while, you want to do something messy, and you end up needing to write out the lifetimes manually. This is when you end up seeing weird things like "'a". But in my experience, this is pretty rare unless I'm doing something hairy. And if I'm doing something hairy, I'm just as happy to have more explicit documentation in the source code.

Really, the underlying problem here is that (a) Rust fills the same high-control, high-performance niche as C++, but (b) Rust prefers explicit control where C++ sometimes offers magic, invisible conversions. (Yes, I declare all my C++ constructors "explicit" and avoid conversion operators.)

Syntax is a hard problem, and I've struggled to get syntax right for even tiny languages. But syntax for languages with low-level control is an even harder problem. At some point, you just need to make a decision and get used to it.

In practice, I really enjoy writing Rust. It's definitely not as simple as Ruby, Python or Go. But it fills a very different ecological niche, with finer-grained control over memory representations, and support for generics.


> The thing is, these dependencies do exist no matter what language you use

Sure, but in a lot of cases, these invariants can be trivially explained, or intuitive enough that it wouldn't even need explanation. While in Rust, you can easily spend a full day just explaining it to the compiler.

I remember spending litteral _days_ tweaking intricate lifetimes and scopes just to promise Rust that some variables won't be used _after_ a thread finishes.

Some things I even never managed to be able to express in Rust, even if trivial in C, so I just rely on having a C core library for the hot path, and use it from Rust.

Overall, performance sensitive lifetime and memory management in Rust (especially in multithreaded contexts) often comes down to:

1) Do it in _sane_ Rust, and copy everything all over the place, use fancy smart pointers, etc.

2) Do it in a performant manner, without useless copies, without over the top memory management, but prepare a week of frustrating development and a PhD in Rust idiosyncrasies.


> My theory is that this removes direct understanding what happens and instead shifts the focus on how to get the task done while not violating the type system rules

In my experience it is in fact still a requirement to understand what's happening in order to write clean and fast Rust code. It is possible to satisfy the compiler using a lot of clone() calls and similar shortcuts, but once you understand what data structure is allocated when and where and how the whole ownership principle works in practice you can suddenly avoid a ton of unnecessary heap allocations and other pitfalls. I'd say my experience with Rust improved a lot once I started grasping what's happening in memory and why certain syntax elements exist, as this makes fixing compiler errors more intuitive and less a blind trial and error session with the borrow checker.

Some of Rust's syntax can look weirdly unfamiliar at the beginning, but everything is there for a good reason. Nothing is done implicitly, and much of the pattern matching and Option/Result related stuff is Rust's way of replacing the concept of `null` with thoroughly type-checked expressions.


> Rust: Rust sits at an even lower level than D. I really like the higher level ideas and syntax but I absolutely don't want to think about ownership issues. If that part of the language were removed Rust would be pretty ideal. But then it wouldn't be Rust.

Ownership isn't a “low-level” issue. It arises in every program that manipulates ephemeral resources (like network connections), and you have to reason about it, whether the type checker forces you to or not.

> Manually controlling the ownership process is a core part of the language and would be pretty hard to change.

It's actually pretty automatic. You only need to implement each destructor once. Using `finally` or `defer` is what is needlessly manual.


> coding in Rust is like playing some kind of intricate puzzle game. Tricking the compiler into accepting your code

That is mostly only true for very early beginners.

There are certain patterns you have to learn and understand, especially for developers not accustomed to thinking about lifetimes ( which applies to most developers that only have experience with GC languages).

And sometimes you do have to battle the compiler, even with experience.

But most of that goes away pretty quickly once the language clicks for you.

After that Rust can still feel restrictive, but that's because there are very few languages that enforce as much correctness at compile time.

That very well can mean that Rust just isn't a good fit for certain domains - which is perfectly fine!


> It seems true to me that working with the Rust borrow checker is eventually fruitful, but there is a micro-case where working with lifetimes seems like a lot mental overhead with little gain, compared to a built-in garbage collector available in Lisp.

Fwiw, my shop uses Rust in some basic web applications and lifetimes are not used in day to day programming. It heavily depends on what you're doing. Are there some applications/libraries/etc where we're using lifetimes? Definitely. But for us they're very niche. AND, perhaps most importantly, even in those you _still_ don't have to. If you're okay with the performance trade off, which we most certainly are (we have some Python usage for example), adding in basic reference counting can be a no-brainer tradeoff.

I actually quite like lifetimes. I have some side projects where i use them extensively. But just like standard Generics they're a tool you can use, but that doesn't mean you always should. Just like accepting a `s: &str` instead of `s: S) where S: AsRef<str>` can make your life cleaner and simpler, using `Rc<Foo>` can equally simplify.

Just my 2c :)

[1]: because we like it, no other reason.. really, a team of ~30.


> For example, I feel it's just too easy to call `.clone()` to store an owned value instead of storing a reference and introducing the correct lifetime constraints.

There's three levels when on the ownership ladder:

1. Taking ownership of an object: this means your have mutability and 'static lifetime. This often implies cloning though.

2. Having the object behind a ref-counted pointer (Rc or Arc in multi-trhreaded scenario) this way you have 'static but not mutability (unless you're using interior mutability with RefCell/Mutex but now you have to think about deadlock and all)

3. Taking shared references (&) to your object. It's the most performant but now you have to deal with lifetimes so it's not always worth it.

Rust beginners often jump from 1. to 3. (Or don't because “too tedious”) but 2. is a sweet spot that works in many cases and should not be overlooked!


>the borrow checker, I do conceptually understand lifetimes, but actually using them is tricky.

I've been using Rust for a little over year, almost daily at work, and for several projects. I have a pretty good intuition about how the borrow checker works and what needs to be done to appease it. That said, I don't think I'm any closer to understanding lifetimes. I know conceptually how they are supposed to work (I need the reference to X to last as long as Y), but anytime I think I have a situation that could be made better with lifetimes, I can't seem to get the compiler to understand what I'm trying to do. On top of that very little of my code, and the code I read actually uses lifetimes.


> Isn't part of the point of Rust that you don't manage memory yourself, and rather that the compiler is smart enough to manage it for you?

For trivial cases, kind of. But once you start to do anything remotely sophisticated, no. Everything you do in Rust is checked w.r.t. memory management, but you still need to make many choices about it. All the stuff about lifetimes, borrowing, etc: that's memory management. The compiler's checking it for you, but you still need to design stuff sanely, with memory management (and the checking thereof) in mind. It's easy to back yourself into a corner if you ignore this.


> Most folks like me that are doing application programming don’t need Rust’s full feature set. You can benefit from type safety/performance/etc even with a subset of the language.

I do think Rust suffers sometimes from guides putting all the complication upfront. When I first started playing with Rust lifetimes confused me… so I used Rc<> everywhere I’d need to use a lifetime and everything worked great. Then, later, I got to grips with how lifetimes actually work.

The vast majority of Rust applications aren’t so performance centric that a little reference counting would kill them. But you rarely see it recommended because it’s not the Rust Way.


>You'll find yourself constantly fighting the borrow checker.

In the beginning, yes, this is true. But most people learn within a month or two which design patterns lead to problems with the borrow checker and which work smoothly, and often this knowledge translates to good design in languages like C and C++ as well.

If you're fighting the borrow checker in Rust, you'd probably have been fighting segfaults and use-after-free in C / C++. I'd rather spend 30 minutes fighting the borrow checker than spend 4 hours digging around in Valgrind.

> Type signatures are littered with lifetime annotations.

You cannot avoid the concept of lifetimes, without a garbage collector. If you don't want garbage collection, you have to deal with them.

Having explicit lifetime annotations in the code is _vastly_ better than trying to track the lifetimes in your head from scratch every time.


> One thing that did just occur to me though is how many adopters of rust were coming from languages with entirely automatic lifetime management (GCs, etc) so having to think about object lifetimes at all is new?

Maybe not "new" but something that's tedious once you've drank the GC koolaid.

With C++ explicit lifetime annotation is never required. You are expected to just do it right and know how long something will live (RAII helps a lot here).

With Rust, when you get into a situation where lifetimes are required, it can often feel really tedious to have to insert them everywhere. Especially when dealing with structs and fields on structs.

Heck, some things in rust simply aren't possible (in safe rust) that are possible with C++. For example, a class with a pointer to itself. In C++'s case you'd say "Well of course this lives as long as itself does so we don't need to free it" In rust, you literally have to use an `unsafe` block to make such a construct.

And, of course, in managed languages you wouldn't take a second thought to why something like this.

This particularly rears it's ugly head when you come from a managed language and you want to do multithreading in rust. Rust's promise of "We can do safe multithreading" sounds REALLY nice, until you learn exactly what that entails. It may be safer, but it's not easier. C++ is easier to multithread than rust (perhaps not correctly) and it's a LOT easier to do correctly in managed languages.


> Rust has a lot of things that C doesn't, like classes, lifetimes, and the borrow checker, which can be jarring to deal with for programmers used to other languages.

If you are a competent C programmer - the kind that "doesn't make" memory errors, you have to be manually keeping track of lifetimes and borrows.

Rust takes that mental effort and offloads it to the compiler.

Lifetimes and the borrow checker will likely be jarring for people coming from languages where you don't need to worry about memory allocation and ownership, but if you are coming from C or C++, you will likely find the borrow checker a relief (I did).

> the compiler won't let you do the "bad" things, so you have to get everything write from the word go.

And it's wonderful! Finding and fixing problems at compile time is the fastest and cheapest way to do it, and is also a productive way to refine your abilities.

next

Legal | privacy