Switching from C++ to Rust
laplab.me
laplab.me
It's one of those patterns that just comes up again and again and it's wonderful when the language offers a good solution.
> i have almost never needed to use such types
Well sure. When is an abstraction "needed"? Before Fortran, nobody "needed" a programming language either. So is a programming language necessary?
That's the funny thing about the word "need." It has very narrow application, and it's precisely why I didn't mention the word a single time in my top level comment.
If you're asking to compare tagged unions and unions, then...
A union is not a tagged union. A union is part of a tagged union. A union on its own is just a region of memory. What's in that memory? I dunno. Who does? Maybe something about your program knows what it is. Maybe not. But what if you need to know what it is and some other aspect of your program doesn't tell you what it is? Well, you instead put a little bit of memory next to your union. Perhaps it an integer. 0 means the memory is a 32-bit signed integer. 1 means it's a NUL terminated string. 2 means it's a 'struct dirent'. The point is, that integer is a tag. The combination of a union and a tag is a tagged union.
An abstract syntax tree is a classic example of a tagged union.
If instead you're asking to compare tagged unions and sum types, then...
Tagged unions are one particularly popular implementation choice of a sum type. Usually sum types have additional stuff layered on top of them that a simple tagged union does not have. For example, pattern matching and exhaustiveness checking.
In that case, the pattern matching and exhaustiveness means that the value the sumtypes in rust offer is in combination with those other language features, not the mere usage of the sumtypes themselves then, I think that makes more sense.
The discussion around "sum type" the concept is to disentangle it from the implementation strategy. Tagged unions are the implementation strategy, and they don't come with pattern matching or exhaustiveness checks.
Of course, many of these terms are used interchangeably in common vernacular. But if you look at the comment that kicked off this annoying sub-thread:
> so, um, unions? i have to say that in many years of programming, i have almost never needed to use such types.
Then this is clearly trying to draw an equivalence between two different terms, but where no such equivalence exists. There's no acknowledgment here of the difference between what a "union" is (nevermind a tagged union) and the "sum types" being talked about in my original top level comment. And the last part, "i have almost never needed to use such types" is puzzling on multiple levels.
no - one of the advantages of OO programing is that class hierarchies (should you feel the need to use them, which mostly i do not) can be expanded. or indeed contracted.
> Or variables where certain values have special case meaning?
very, very rarely (i would say never, in my own code) but of course we have the null pointer as a counter-example. to which i can say: don't use raw pointers.
It doesn’t make OOP obsolete, though, at all. Certain other areas are better expressed as hierarchies, e.g. GUI nodes.
https://fsharpforfunandprofit.com/series/designing-with-type...
By the third entry in the linked series, we've motivated the use of a sum type with three variants in the example domain model.
Enums + switch is the other, and far less powerful.
Switch expressions were also implemented, and they can exhaustively match on the given sum type. Pattern matching is quite limited as of now (only usable with records), but it is coming.
The problem with Java was always that it has moved so slowly in acquiring nice things. The actual core runtime is amazingly well engineered.
Relatedly, storing booleans is a smell, imho typically an enum or sum type is always better in languages that have concise syntax for these. True and False are meaningless without context, and so can easily lead to errors where they are provided to a different context or generally misused due to a misunderstanding about the meaning.
i agree completely with your second point - passing around or storing booleans is usually horrible.
No fanciness needed, just plain old sum types. It is certainly possible to express those invariants directly in languages with a dependent type systems or refinement types like in liquid haskell - see https://ucsd-progsys.github.io/liquidhaskell-tutorial/Tutori.... It's typically much easier to reason about and use sum types, though.
Of course these examples are trivial and silly, but I see instances of these patterns all the time in big co software, and of course usually the invariants are far more complex but many could be expressed via sum types. I've seen loads of bugs from constructing data that invalidates assumptions made elsewhere that could have been prevented by sum types, as well as lots of confusion among engineers about which states some data can have.
> Field X is only set if field Y is true
Original gnarly C style pattern:
struct TurboEncabulatorConfig {
// When true, the turbo-encabulator must reticulate splines, and 'splines'
// must be non-null. When false, 'splines' must be null.
bool reticulate_splines;
struct Splines *splines;
};
Rust (let's ignore pointer vs value distinction): enum TurboEncabulatorConfig {
NonReticulatingConfig,
ReticulatingConfig { splines: Splines },
}
> If field X is non-null then field Y must be null and vice-versa.Original gnarly C style pattern:
struct TurboEncabulatorConfig {
// When non-null, lunar_waneshaft must be null.
struct Fan *pentametric_fan;
// When non-null, pentametric_fan must be null.
struct Shaft *lunar_waneshaft;
};
Rust: enum TurboEncabulatorConfig {
PentametricTurboEncabulator { pentametric_fan: Fan },
LunarTurboEncabulator { lunar_waneshaft: Shaft },
}If we cannot run it at runtime now we need tests to hit that path or to test it manually to actually know if it works.
It's less awkward with Rust's enums for sure though. And pattern matching as in Rust is far more expressive (and legible) than what std::variant gives you.
https://gist.github.com/erinok/c823af95db408653c7e42ab189307...
That was removed in C++11. The rule now is:
> Absent default member initializers ([class.mem]), if any non-static data member of a union has a non-trivial default constructor ([class.default.ctor]), copy constructor, move constructor ([class.copy.ctor]), copy assignment operator, move assignment operator ([class.copy.assign]), or destructor ([class.dtor]), the corresponding member function of the union must be user-provided or it will be implicitly deleted ([dcl.fct.def.delete]) for the union.
i.e., you need to provide an explicit version of the special function for the union if any member has a nontrivial implementation.
So you could have a struct or class with one std::variant field and some methods which can match on the type of the variant. But it would be kind of clunky.
In C, you have to wrap the union in a struct in order to add the tag, and that pattern is fantastically common. Let's have a little geometry-inspired example:
typedef enum { SHAPETYPE_RECTANGLE, ... } ShapeType;
typedef struct { ... } Rectangle;
typedef struct { ... } Circle;
typedef struct { ... } Triangle;
typedef struct { ... } Polygon;
typedef struct { // Outer struct, not a union at this level.
ShapeType type;
union {
Rectangle rectangle;
Circle circle;
Triangle triangle;
Polygon polygon;
} // This can be nameless in new(ish) C, which is nice.
} Shape;
Then you'd create a value like this, maybe: Shape rect = { .type = SHAPETYPE_RECTANGLE, .rectangle = { 0, 0, 20, 10 } };
Of course wrapping the initialization in a function would make it nicer.The above union-in-a-struct wrapping is my mental model for how enums work in Rust, but I still find it jarring. :)
So for your example you put ShapeType type in each of Rectangle, Circle, Triangle etc. and then you can union all of them, and the language promises that shape.circle.type == Rectangle is a reasonable thing to ask, so you can use that to make a discriminated union.
That doesn't sound right to me. Do you have a source? Is that in the standard?
11.5.1 [class.union.general]
[Note 1: One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence ([class.mem]), and if a non-static data member of an object of this standard-layout union type is active and is one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of the standard-layout struct members; see [class.mem]. — end note]
So yeah, it's possible to store the tag in the common initial sequence of all the union members.
This is quite niche and rarely used. Most of the time it makes more sense to have the tag outside. Sometimes the tag in the common initial sequence allows the whole data structure to pack better than with a tag outside of the union.
C++ unions can actually have methods, although this isn't used very much. However C++ enums can't have methods, even C++ 11 scoped enums ("enum classes") can't have methods, I have no idea why that restriction seemed like a good idea.
† Unions are special because they're crazy dangerous, which is why they're not usually covered in material for learning Rust - you can't fetch from them safely. You can store things in unions safely because the process of storing a value in a union tells the compiler which is the valid representation - the one you're storing to, but fetching is unsafe because you might fetch an inactive representation and that's UB. However Rust does have a particularly obvious union right in the standard library - MaybeUninit - and sure enough MaybeUninit implements Copy and has a bunch of methods.
It is possible that Oracle holding the patent[1] to methods on enums is the blocker, rather than any technical restriction.
I don't think programmers often consult 20 year old "inventions", so it seems pretty obvious on its face that the supposed benefit of patents, that something is _only_ locked up for 20 years, is quite pointless in software.
Anyway, for loops are safe, unless the for loop is over the elements of a linked list. Then you need to wait until next year: https://patents.google.com/patent/US7028023B2
Patents really have a chilling effect, even if a particular one might not be enforceable.
Having said that this is the first time I ever heard of methods on enums being patented, what a ridiculous patent. It's a good thing then that C++ doesn't have methods, it has "member functions" :).
Also C++ allows user defined operators on enums, which feels somewhat adjacent.
In the case of MPEG the result is MPEG LA, a US company which you need to pay to implement certain important standards. In the case of JPEG the result was a little different, since only the improved Arithmetic Coding of JPEG was patented, people just don't implement the actual standard, they cut out the patented part, so all the world's JPEGs (well, mostly JFIF files, which are slightly different but we call them "JPEGs" anyway) are a little bigger than they need to be for no reason except patents.
So no, I don't buy that "ISO is also very averse of patents" in a sense that would restrict this unless you can show that's a new stance.
enum X {
A,
B,
}
fn foo() -> X::A {
X::A
}
which helps a lot when composing state machines. In the meantime, you can indeed do what you propose: enum X {
A(Foo),
B(Bar),
}
struct Foo;
struct Bar;
fn foo() -> Foo {
Foo
}(And C++ std::optional<T> is somewhat similar to Rust's Option<T> type.)
https://github.com/facebook/folly/blob/main/folly/Expected.h...
namespace our_project {
class Status { ... };
template <class T>
using Expected = folly::Expected<T, Status>;
}
absl::StatusOr<T> looks a lot like our_project::Expected<T>. This pattern of providing your own error type and aliasing Result is also somewhat common in Rust, I believe. const number = try parseU64(str, 10); // returns the error
const number = parseU64(str, 10) catch 13; // default
// more complex stuff
if (parseU64(str, 10)) |number| {
doSomethingWithNumber(number);
} else |err| switch (err) {
error.Overflow => {
// handle overflow...
},
// we promise that InvalidChar won't happen (or crash in debug mode if it does)
error.InvalidChar => unreachable,
}https://en.cppreference.com/w/cpp/utility/expected
Unfortunately it does not come with nice syntax to return on error.
> Unfortunately it does not come with nice syntax to return on error.
Yeah. We use a C preprocessor macro for this, but it's a little more verbose than question-mark operator.
I'm just nitpicking but Rust is 7 years old and 7 doesn't feel almost 10...
Edit: Rust hit 1.0 7 years ago. Now I feel silly.
First spike at Google Trends for "Rust (Programming Language)" happened back in late 2013/early 2014 (https://trends.google.com/trends/explore?date=all&q=%2Fm%2F0...) so not at all impossible for someone to have ~10 years experience with it. Although that's probably pretty uncommon.
As a bonus, here is a very old (by internet time standards) HN submission titled "Mozilla releases version 0.1 of the Rust programming language (mail.mozilla.org)" - 236 points | Jan 23, 2012 | 82 comments - https://news.ycombinator.com/item?id=3501980
This is my first public commit: https://github.com/BurntSushi/quickcheck/commit/c9eb2884d6a6...
I didn't write any substantive Rust before that point. So I'm at over 9 years.
Edit: Oh uh you're burntsushi. Should've checked your username.
Rust has been around for well over a decade - the first “stable” release was 7 years ago. It’s always been opensource - any random hacker could download it and start using it since it was available. I think you will find kind quite a few people on this site that took it for a spin when it was still under development.
I have nowhere near the credits of BurntSushi, and even I was dabbling with rust more than 7 years ago.
Don’t be a reply guy.
Dynamically-typed languages often have adhoc sum types.
Suppose I say that cars without seatbelts are "unfit" to use on public roads. Clearly I can't mean it's impossible to use such cars, people used to do it all the time, but perhaps I mean it's a bad idea to use them, and that's harder to argue with which is why we got laws saying you need seatbelts.
[1] unless you liberally use `union`s everywhere which is very unidiomatic C++, but can be done if desired.
For what it's worth, Python's type hints are also Turing complete (https://arxiv.org/abs/2208.14755, discussed on HN at https://news.ycombinator.com/item?id=32779296)
https://developers.redhat.com/blog/2017/11/16/speed-python-u...
- nobody uses it in the ecosystem. As outlined in the article, a lot of value of Option/Result is derived from their pervasiveness in the Rust ecosystem. C++ is far from this
- the ergonomics of it are terrible: no pattern matching, structural variants instead of named variants (yes you can emulate that with wrapper types but meh), lambda-oriented matching means you cannot as easily do things like early returns, statement-oriented language limits the usefulness anyway, lack of combinators, and for error handling specifically, lack of `?` operator
- performance is dubious. I had very steep and unexpected performance cliffs when lambda inlining started to fail for some reason. Having sum types be a language construct guarantees we're not relying on things like lambda optimisation here.
Error types are probably the most popular incarnation of that. There’s several libraries available and they will be part of the C++ standard.
This is a case of the Rust community overselling a minor feature as a game-changing novelty.
I think you misunderstood me though if you say I haven’t used them properly. I just didn’t need them or find them that useful because it rarely happens that I want to model a type which has several states.
Specific variant types like std::optional or the Result-equivalents are useful, but not that game changing either. They’re nice I suppose.
I wonder what kind of software you write that such low-level coding idioms make a big difference to the end result. Or what do you mean by game changing?
You can't use what you don't have, so you adapt to the tools you do have. In my C++ time, the team would often write types that logically held several variants. However they were expressed as product types, so with space overhead and error-prone, unergonomic use.
Sum types are game-changing. Look at any Rust project you'll find enums with data everywhere. They're just a building block to model problems, like product types are. A language that miss them is as strange too me as a language without product types. After 10 years of mostly C++11 and C++14 I would never go back to it for this reason alone (although as outlined in the article there are other reasons too).
> Error types are probably the most popular incarnation of that. There’s several libraries available and they will be part of the C++ standard.
Again, with what performance and ergonomics?
In matcher.rs, the enum is used as poor man’s OOP. I saw several match expressions which then call the same function on each matched type.
searcher/mod.rs is an error definition.
glob.rs contains a sort of policy enum which can be implemented once again with OOP or as a policy template.
json.rs usage can be modeled as a single class.
core/app.rs is more involved, but can be modeled as a series of structs with a map from enum -> any. Or as am std::variant. Or using OOP.
I looked at all instances and didn’t see anything game-changing. It’s a nice syntax and it should have really good performance, but such idioms are way too low-level to change any game.
As should be patently fucking obvious to anyone who has been commenting on a technology web site for as long as you have, sum types are a tool. They are a tool for expressing clearly and concisely the idea that a value can be exactly one of several possible options. That tool then interacts with the rest of the language based on that invariant, sometimes providing things like exhaustiveness checking and pattern matching. Put all this together, and you have a very succinct and very clear way of representing certain kinds of values in a program.
Your comment might as well go through ripgrep and talk about how functions aren't needed. "They could have just used goto here and there."
> I looked at all instances and didn’t see anything game-changing. It’s a nice syntax and it should have really good performance, but such idioms are way too low-level to change any game.
Sum types were game changing to me when I learned about them over a decade ago. Since then, they have been a significant factor in how I think about and structure data in programs.
I have zero interest in trying to convince someone like you that you should think it's game changing. That's not the point. Maybe you could do some perspective taking and realize that others might just think differently than you.
Sum types are not novel (Standard ML had them 40 years ago), but I think they are indeed a game changer.
Sum types reify control flow into an object from which said control flow can be retrieved. Compiler checked sum types remove the possibility of retrieving inconsistent control flow.
The transform is equivalent to callback to future.
It's one of the things that looks unimportant until you use it. After that, the absence is repeatedly experienced when working with C++. We don't use tagged unions much because the ergonomics are terrible.
The choice to provide exceptions everywhere as a error handling means C++ is obliged to admit that your std::variant may not have a value at all. Which blows up all of your type safety. In Rust I can say that this Pet is either a Dog or a Cat, and it cannot be neither, but in C++ std::variant of a Cat and a Dog might nevertheless be valueless_by_exception anyway. What can you do about that? I guess you could throw an exception...
In Rust if X is either A or B, and Y is either C or D, and Z is either E or F, then a structure with X, Y and Z has only eight possible states. In C++ this structure has 27 possible states because of exceptions.
Wrt type safety we had more pressing issues with C++ (use after move, implicit conversions, ...)
You also don't have to handle the exception, in which case you won't access the variant again anyway. Or it gets handled where the variant is teared down. It's very unlikely that it gets handled where the variant is constructed or assigned to, making it a non-issue.
Granted, it's more awkward when you consume 3rd party libraries, but you can still wrap them in noexcept interfaces and be fine with terminating when an exception is actually thrown or do something else. Not much different to a panic.
Since 2017, there is std::variant, which is a sum type template that _technically_ allows for language-level exhaustiveness checking via std::visit. It's not pretty, but it gets you there without requiring compiler support.
Rust's version looks way better.
Most modern C++ codebases do compile-time polymorphism via templates, though. std::variant is a niche use for things like serialization, when you need to make type choices at runtime.
It's just a pain to use because of how much template magic is used.
Seriously though, calling Algebraic Data Types an Enum was a stroke of genius.
[0]: https://dr-knz.net/rust-for-functional-programmers.html
A "Rust enum" is a sum type.
Also, I talked about ASTs being a good example use case for a sum type. Here's a "real" version of an AST for a regular expression in all its complex glory: https://github.com/rust-lang/regex/blob/a9b2e02352db92ce1f6e...
You can see that it starts out as a sum type at the top level. And inside each variant is all sorts of product types and other sum types.
But do note in practice that sum types aren't limited to specialized things like ASTs. In practice, they come up everywhere. For example, here's a small little state machine used to implement a simple "unescape" routine. e.g., converting the string 'a\xFF\t' to the byte sequence 0x61 0xFF 0x09: https://github.com/BurntSushi/ripgrep/blob/44fb9fce2c1ee1a86...
That said, the bulk of our code is Nim, and it is lovely to work in. I just wish we didn't have to rely on C/C++ libraries and toolchains as much. CMake will be the death of me, I swear, and it would be nice to have all the thought we have to put into to lifetimes and safe bindings around unsafe C calls be able to be encoded into the language itself, rather than as comments and so on.
Having used their tools at work in anger for years at this point, there is a lot of gotchas, issues and bugs with their bindings at this point -- not all of which is Rust's fault, but the underlying problems with ESP-IDF itself.
That said, its getting better over time, so I'm keeping a close eye on it (and we keep some experimental projects up to test it every couple of months).
The upside of Nim is that "it's just C" at the end, with all the upsides and downsides that comes with.
I'm very hopeful though!
Edit to add: We started this project quite a while ago, too, which forced our hands. Writing unsafe Rust is a pain, so I didn't want to take the burden on for managing bindings myself at the start. If we were to start again, maybe we'd have made a different choice, but Nim at least eased the pain quite a bit.
Definitely keeping an eye on it though. The future of Rust on the ESP32 chips is promising!
[0]: https://github.com/esp-rs/esp-wifi#current-support [1]: https://github.com/esp-rs/esp-wifi#ble
What Espressif is doing with their esp-idf and porting it to Rust is promising, but overall it still needs work. Using the toolchain to develop on the ESP32 was at least slightly painful half a year ago before they introduced espup[1], having to keep a patched LLVM around etc., and supposedly support for their Xtensa architecture is coming to LLVM soon[2] so this will improve in the future.
I'd also love to see Bluetooth support in esp-idf-svc[3], but they seem to be lacking people with the required knowledge to design and implement an abstraction for it[4].
[1]: https://github.com/esp-rs/espup [2]: https://mabez.dev/blog/posts/esp-rust-24-02-2023/ [3]: https://github.com/esp-rs/esp-idf-svc [4]: https://github.com/esp-rs/esp-idf-svc/issues/55#issuecomment...
I appreciate the inclusion of this section. Different people writing C++ can have wildly different experiences, based on the type of things they are working on. This section offers a good reminder about this, and that one person's perspective is not more or less correct than anyone else's.
The difference is that the use of the smart pointers is checked of the compiler
I've got a pretty big C++ codebase for my hobby projects, sanded down, polished and perfected over the years without the usual corporate pressure to ship. The few times I run into memory corruption, a memory leak, a segfault, dereferencing a shit pointer, undefined behavior, and so on, its always, always because I'm doing something I shouldn't be doing. Like working with raw pointers or pointers to pointers to pointers, or traversing an array of bytes to do something there's already a library that does, or manually calling delete on something, or using reinterpret_cast<>, or using one of the many footguns C++ happily gives me. The simple key is to just stop doing these unnecessary things.
It's not what C++ can or cannot do, it's not what Rust can or cannot do. It's what the Rust language and compiler opinionatedly encourage you to do and not to do. This is what's lacking in C++.
You've never used an out-of-bounds index? Accidentally used an object that was on the stack beyond the function call? Let an integer overflow? The problem with C++ is that all of these things don't look like unsafe operations, and people end up making mistakes with them that are not obvious.
Integer overflow can be turned into defined behavior through a compiler switch.
Holding pointers to stack variables past the return is not as easy to inadvertently do as an off-by-one. Someone has to store the address into a variable and this kind of code should raise alarm bells.
A more indirect way is to pass an address of a local variable to a function which is typical. But then if a function takes arbitrary pointers it receives and stores them, this should also raise some alarm bells.
Can't speak for the OP, but I never see any of these bugs in my C++ code. You can write code with these bugs but that is more of a style choice, practically speaking. The C++ bugs I see are almost always in complex state logic or in rarer cases an unexpected/unhandled error case from a call to outside code, which can happen in every language.
The question is how often you mess up in each language and how bad those mess-ups are. This depends a lot on the programmer(s) and the kind of project. My personal view is that the space of team/project combinations where you should ever start a new C++ project is now confined to "team desperately wants C++".
I think even now the best resource to learn C++ is to first learn C, then original C++, then all the modern memory management techniques, which is just crazy hard for a new programmer compared to just going through the Rust book 10 times (which is needed to get a deep understanding).
I'll let you decide whether this is sarcasm - I could go either way.
I'm with you, my sunken cost is 10 years, though.
nobody with an inch of nous does that anymore.
Especially when performance matters (eg: high frequency trading systems, video games, constrained embedded systems, and more), the improved locality that you can get from custom memory management can be worth it all on its own.
For a good talk on this subject, see Lakos "Local Memory Allocators" presentation:
* part 1: https://www.youtube.com/watch?v=nZNd5FjSquk, part 2: https://www.youtube.com/watch?v=CFzuFNSpycI
Types that implement Copy are still moved. Copy just means “the original value is still usable after move”.
Makes me wonder out loud if the C++ community isn't going to add some kind of std::borrow/std::return_borrowed to the language. If that's even feasible with the type system and reference system as it is today.
Perhaps the only hard part is storing and passing references everywhere, which may mean that one has to act like an automaton and patiently type their lifetimes to the Rust compiler. Unfortunately the Rust programming community has settled exactly on this kind of reference usage.
Rust is a huge win for the small niches where the GC overhead is unacceptable, because it is completely novel in its memory safety, which is absolutely a must and was neglected for way too long, but I never really understood the desire to use it for CRUD apps and the like. Sure, to each their own, but it is just an arguably bad choice. Even OSs could be written in managed languages - it’s not like it hasn’t been done before and they can just as well have escape hatches like Rust’s unsafe (hell, they are likely even safer if they are only used to manipulate “external heap”)
I suspect it was just happy circumstance that a type system so strong it makes memory access compile time checkable is also strong enough to eliminate the overheads of GC. So they did that too.
But the claim that all this tediousness is there to eliminate GC sort of misses the point - that's not the reason they introduced it. Rust's complex type system is a type of formal proof system that eliminates a lot of bugs at compile time. The "sum types" discussion above talks about another class of bugs it eliminates. Eliminating bugs at compile time is the point - not making memory allocation easier.
And to be honest, Rust doesn’t have all that strong type system compared to Haskell/ML which introduced these concepts in the first place. Also, there are plenty of languages in the category of “managed, with a strong type system”.
While I agree with everything in the article, some times I wish I could have it both ways with:
> First of all, generics without duck typing are greatly appreciated. Traits clearly indicate the contract struct or function expects from the type, which is great. This also helps compiler to generate helpful error messages. Instead of “invalid reference to method clone() on line Y” you get “type X does not implement Clone” - clean and informative.
I will say that there are times when I wish I could sneak in some duck typed traits, particularly `Debug`. Sometimes you are working in generic or dynamic dispatch code and just want to use `dbg!` but can't because (1) you didn't think ahead or were too lazy to annotate things with `Debug`, (2) you are a bit paranoid about excluding a type that doesn't support `Debug`, or (3) You are doing dynamic dispatch and `FooTrait + Debug` only works for generics.
`Debug` is also just one example. Overall, I love the explicitness but at times I do wish I could fudge things a little.
I'm glad to hear about your experience with the language as a developer. It's also been my experience that Rust has the magical power where your compile errors are almost always logic bugs. That's pretty cool from a psychological perspective, because you really feel like your C++ compiler is your enemy (or worse, your lawyer) while the Rust compiler is your friend.
The C++ developer experience is absolutely horrendous (with far too much of the unholiness that is cmake), and I hope that shedding some light on the situation can help people understand how to make it better.
After getting used to the basics of Rust, I found the cognitive load to become much lower compared to C++. There's just no way I could write correct C++ code in such a careless fashion.
So now I can spend brain cycles on things that actually matter, and it's great.
I was working on a card game simulator and I had a Vec of players. I needed to pull two players from that Vec and the first player would give a card to the second player. In my head I would grab both players via get_mut and then perform my operation. However, get_mut borrows the vec mutable and the compiler complained that I borrowed the Vec mutably two times.
It took me a bit to understand why the compiler complained, but then it clicked: It couldn't prove that get_mut wasn't returning the same item both times.
There were a few solutions. One was to borrow the first player, take a card, drop the &mut and then take the second player. At some point in the future I could use https://github.com/rust-lang/rust/issues/104642 to get_many_mut. I ended up with a pretty inefficient version of get_many_mut that fully traversed my iterator to get the two mut references (which works because traversing the iterator guarantees you won't see the same element twice) and it was fine for a collection of a half dozen players.
Anyway, there's a little example.
Anyway, it was a small thing but
> you have to junk the object-oriented design entirely
I think it's not this black-and-white. OOP on a higher level is about encapsulation and message passing. Such design is perfectly doable in Rust. I'd say that it's more natural in Rust due to it's `impl` concept: where behaviour and data are coupled in a way that's different from most OOP languages.
Who owns which data, and who can operate on it, is something that, in OOP, should be thought about just as well. Java, Ruby, et al make it easy to make a mess from this; yet that doesn't make all OO-design a mess. Ihat really is "bad use of OOP", and no reason to "junk the OO design entirely". At most it's "junk the bad OO design".
http://dtrace.org/blogs/bmc/2018/09/18/falling-in-love-with-...
Basically he moves from his homegrown pointer chasing looking a lot like a doubly linked list to just using a hashmap and putting the key in his structs. As a naive first approach to satisfy the borrow checker, thinking he would sacrifice performance but at least the compiler would be happy.
Spoiler: it got faster. Far faster. To his surprise.
And, as someone who's spelunking in the depths of a large CMake project right now, cargo sounds pretty nice.
Bryan Cantrill has a number of talks about Rust which are similarly pragmatic
I do this for personal projects and honestly, considering the alternatives, I think it rules.
Need to handle creating projects for multiple ides? That’s a pain with bash unless you make an entire library of tools.
Need to handle finding libraries with all their transitive dependencies? Another set of tools.
At that point you’d just have reinvented cmake in a different language.
Out of curiosity, are you using bash for projects that are limited to just yourself?
There's a 'modern' way to do things, and a 'legacy' way to do things, and the respective designers of CMake and C++ have decided that it was—for several reasons—better to leave the 'legacy' stuff in the languages, rather than pull a Python and make a clean split.
In 'modern' CMake, there are targets, and properties on said targets, i.e. compile and link options, C/C++ versions, libraries, headers, other custom dependencies, etc.
There are also functions and generator expressions[1] to make control flow a little easier. On top of these, CMake's built-in `find-package` and `FetchContent` make package management a lot easier than it used to be. Want Boost? Just do `find_package(Boost)`, and then `target_link_libraries(<my_target> Boost::boost)`. It gets easier still with a proper C++ dependency manager like vcpkg or Conan.
In legacy CMake, all these were set with global variables and there was no unified way to handle packages. I fully foresee going forward that at least a plurality of C++ developers will coalesce on a CMake + vcpkg (which has more packages than Conan) workflow.
[1]: https://cmake.org/cmake/help/latest/manual/cmake-generator-e...
One of the pain points of trying to design a build system for C/C++ is that a lot of projects do weird stuff that needs to be supported in build systems, which means you end up needing escape hatches to do that weird stuff all over the place. Cargo takes a narrower view, which means it doesn't attempt to do things like package things for install or handle multi-stage builds. It also has the advantage that things like running tests or package dependencies were built in from the start, so it doesn't have to try to support a million different tools that provide that functionality.
Technically nothing stops C++ from having the same, except getting everyone to agree on one tool.
Doesn't Rust require you to switch to nightly to use sanitizers?
I had no chance to meet it yet, nor I have currently time to experiment, thus I was curious about the article to help me understand the differences. The only thing I remembered from the article is that compiler error messaging are different in the two and more clearly defined in RUST. Plus classical anti C++ example => memory management. C#, Java already addressed that, so I expect RUST has something more unique than that. Can somebody point to better article that shows some practical appliances why to switch to RUST if you have significant C++ experience?
Not that I want to bitch the author. Nice try explaining something, bue you know fixing error messages in C++ code was never a random staring at the code in my case.
We wanted to get rid of C++ exceptions but traded it for panic hell.
Which brings me to the point: propagating errors is tedious enough that people are looking for shortcuts, which then cause other problems.
I think you maybe meant converting between errors in Rust can be tedious sometimes, but propagation is pretty easy.
The conversation is something that generally need's to happen rarely (like a few times per project?), but `thiserror` and `from` make this pretty ergonomic.
At a meta level, one of the reasons why there is so much focus on 'unwrap()' in particular is because it is often the precise point at which in your code where a runtime invariant is broken and thus leads to a panic. Nearly all code has runtime invariants in one form or another. The question is what happens when they're broken. In languages that are memory unsafe by default, the answer is often (but not always) "undefined behavior." In languages like Rust, or Python, or Go, the answer is often (but not always) "the process quits." The reality is more complicated than that, but those are a fine first approximation. For example, breaking a runtime invariant doesn't have to lead to undefined behavior or process termination. It can simply result in a logic error that leads to unexpected behavior.
Of course, making the issue more complicated is that sometimes 'unwrap()' is abused. And indeed, sometimes it is used in cases where an error ought to be returned. I find this to be generally pretty rare in popular libraries. But the key point here is that you can't just say, "oh I see unwrap() in a library, so now I'm going to scream ABUSE!!!!" It's more complicated than that.
Result is being sold on HN as a great solution to error handling while there’s several crates available which are needed to polish its rough edges, rough edges which have led to e.g. unwrap abuse in the past.
It is tedious to shuffle around results, but this is never part of the sales pitch. And for those of us that don’t want to depend on all sorts of crates for the simplest things, that’s the reality.
Re your reply “Maybe improve your reading comprehension. I said "(partial) nonsense." Therefore, some of what you said wasn't nonsense. But some is. What a fucking revelation.”
Partial nonsense isn’t nonsense by definition, I believe, but I won’t dwell that point. I am surprised seeing a Rustacean being (mildly) offensive and even saying the f-word. I must say that I am proud of you, too many nowadays act like polite, pedantic robots. :-)
My tactic is to present nuance. You come back to me and instead of actually engaging with the nuance, starting whinging about what's being "sold on HN." Yawn.
> It is tedious to shuffle around results, but this is never part of the sales pitch.
This is a good example. Your sentence starts with something that I wouldn't agree with as a general rule, but I could certainly see it being true in specific circumstances. And even then, I'd want to explore those circumstances. That is, is it really a property of `Result` that makes it tedious, or is the problem of error handling in that context itself? It could be either.
But then you follow it up with whinging about "sales pitch." Are you talking about some kind of sales pitch put out by the Rust project? Because if so, please link it to me. Or are you talking about a bunch of random HN commentators that can be overzealous about just literally anything? I don't see anyone saying Result is the best thing since sliced bread. What I do see are people describing positive experiences with it, especially in relationship to alternative error handling paradigms. Is that really a sales pitch?
I mean, look at my top-level comment in this entire post. I sang the praise of sum types, and I did it by echoing what the OP said. Is that a sales pitch? Am I saying that sum types are the "best" at something? Am I saying that they have literally no costs at all? Am I saying that useful programs can't be written without sum types? Am I saying that all languages should be designed to include sum types?
No. No. No. No and no. Did some other people go a touch too bar and make broad pronouncements about "correct" language design? Oh yeah, absolutely. Are those people some kind of singular phenomenon unique to Rust? Fuck no. And it is absolutely fucking baffling that I need to explain that to someone who has been on HN for as long as you have. This observation is so banal that it's blub.
When I read your top-level comment I don’t read a story from a random developer, I read an endorsement from a prominent member of the Rust community trying to paint Rust in a positive light, but conveniently omitting any negative aspects. This happens way too often to be a coincidence: the polite Rust developer educating others about the benefits of Rust may as well be a recurring character in this series.
Yet as companies actually start to use Rust and hit various problems, these are not given the same amount of attention. Obviously you don’t care about that, but this kind of submarine advertisement is a pet peeve of mine, so don’t be surprised if I continue to comment.
So what I'm getting from your comment is that it's not possible to say something positive about a project like Rust unless all commensurate trade offs are accounted for. Otherwise, the comment is a "submarine advertisement"? I didn't pipe into a conversation about non-Rust. The OP is about Rust. The OP mentioned sum types. I commented endorsing what OP said and to call extra attention to it, because sum types (with pattern matching and exhaustiveness checking) are amazingly useful. I also legitimately do not believe they have many downsides, if any at all. They might have downsides within the context of a particular language design (for example, Go, where their interaction with default values and interfaces would potentially be quite weird). But in general, no, sum types are pretty close to an unmitigated good thing in my view.
Does Rust writ large have downsides? Oh absolutely! So unless you're telling me I need to exhaustively enumerate every downside of Rust every time I mention something positive, then I don't know what you're getting on about.
> so don’t be surprised if I continue to comment.
That's not surprising? You've been posting low quality commentary in Rust threads for literal years. If there are people sick of the "advertising" for Rust, then there are also people who are sick of the people whinging about it. What would be surprising is if you started posting well informed productive comments in Rust topics.
> be careful: if you try to access an index which isn’t in the Vec, your software will panic!
> Use get() and get_mut() if you want to check whether the index is in the Vec.
In my experience, most people are using get() if the source of the index is untrusted
[0]: https://doc.rust-lang.org/std/vec/struct.Vec.html#indexing
Pro tip: most popular crates have excellent documentation (I know it's a shocker coming from other languages). So, check stuff before you use it.
I assume this is because the ecosystem lowers the barrier to entry for writing an generating documentation (compared to other ecosystems).
That's what it's supposed to do, RTFM.
Use .get() for safe access.
Second, I never claimed it was an issue or a bad thing at any point. Someone asked for examples of libraries that panic, and I gave an example.
I must be missing something because it makes no sense to me that people seem to be responding defensively and claiming that I either don’t understand the Rust docs or am pointing out problems in Rust.
Sorry you were misled, but programming is, indeed, hard, and no, you can't go shopping instead.
Obviously this is not something you're encouraged to do often, but there's applications where it might be reasonable.
Exceptions are a particular technique for dealing with errors in general, that can be used for both unrecoverable and recoverable errors. They infamously trade programmer convenience for hard to understand bugs, fragile run-time behavior and unmaintainable code.
Fragile run-time behavior and unmaintainable code is because programming is hard and cruft accumulates in the face of difficult requirements.
Renaming a thing that traumatized you in the past won't make the cause of the trauma go away.
So exactly like C++ exceptions then?
And you usually want the first line, because that's where the line number you actually want to go to is. Perhaps removing repetition and not printing lines of stdlib headers would help.
More a piece? More in pieces? :D
I too got lots of crashes and UB in my life, but more because of pile-of-garbage architecture and interoperability than of language. (un)surprisingly, Rust emerged AFTER people learned their mistakes in C/C++ so it gets lots of attention and expectations.
Err..C++ has concepts. No duck typing required.
Heck, if you ask the average C++ programmer about concepts they'll likely have not even heard of them.
In every language that I can think of that supports generic programming (including Go), there's support for definition checking.
Thanks but no, replacing a bunch of code as strings and then relying on some code _not_ compiling should be left in 20th century for good.
1. how much psychological trauma it brings to average developer because today's most C/C++ library is not shipping static library by default.
2. how much pain it causes when the build system just can not copy the whole damn library into somewhere called lib and include instead spill into hundreds of random locations even we know that it will just build if we just copy it one place.
3. returning a struct with a integer status in it is so challenging for average programmers.
All these must be true because it seems it is the reality.