> Ehhh I think it's not that bad, no harder than arithmetic expression parsing certainly. It's just one more grammar rule. Plus Rust supports everything that's needed already because of operator overloading.No, it forces you to have a cover grammar for things that could be either a type or an expression, i.e. you need to be able to parse the superset of both possibilities, from when you see the '('. This isn't something Rust needs or has at the moment.
> They (well, destructors anyway) are less explicit though. When you use a context manager in Python you know it's cleaning things up. When you "use" a destructor in Rust you usually don't ever know you did. This can get you into trouble if you're relying on RAII to cleanup after you: you can get a handle to things and then enter a long loop or call chain. Sure you can avoid that by calling `drop`, but if context managers were the idiom this kind of thing would never be an issue. But I admit the difference is pretty small -- and in fact might be surprising to systems programmers so probably it's the right choice (even if destructors themselves are kind of mind boggling).
Yeah, that's why I said "most". :)
It's fair that destructors are implicit, but context managers are used for a lot of bread-and-butter clean-up like `with open(filename) as f:` etc, for which the destructor barely does anything.
Additionally, context managers end up being super "infectious": they're much harder to store, manipulate and return than an object with a destructor. E.g. how do you write a function f that opens a file and returns it for the user to use (maybe it does something tricky to find which file to open, or something)? You'd need coroutines or passing a closure into f to be able to manipulate the file handle while its context was open. I don't think the infrastructure required is the right trade off for a systems language (it seems like it'd end up with a fairly strong compile-time vs. runtime performance trade-off, and Rust's compile times are bad enough as they are).
> Well mostly I would deal with all this by removing "everything is an expression". Match blocks would just be regular blocks like `switch` in C or what have you, and so on.
Then you end up with a pile of ceremony and junk, with 'return's everywhere (some functions would get 50% larger/more noisy, just from the 7 characters "return "), and, as with most things, it makes macros and generating code more annoying. However, just to be clear, these changes you're suggesting are making Rust less consistent.
There are many places where Rust is different to C and C++, but it is also more consistent, which is one of your complaints. (You can see "everything is an expression" style of thinking has benefits even in C: the classic do ... while(0) trick for macros, plus GCC's statement expressions.)
> Well switch/if/else have worked fine for a long time; and now C/C++ compilers warn you on missing branches when switching on enumerated values (Rust won't if you have a fallthrough case). But I prefer switch/match to if/else because if/else are too general for just switching on a variable (which is why I'm very dismayed at match guards -- they wholly abrogate the benefit of match over if/else), and I think switch's behavior is generally too restrictive (this is the only problem I have with switch provided you use proper blocks instead of the souped-up goto it really is). Match has so many features baked into it that every time I see one I have to stop and take a deep breath. It's very much geared towards writing and not reading, I feel.
It's not clear to me how much Rust you know from this sentence: you Rust enums do more than C/C++ ones? Each case can contain data, and match is the only way to get at that data. There's no other way to conditionally deconstruct an enum down into its parts, other than 'if let' and 'while let' but those have the problems of if/else.
Having data is the main motivation for match guards: to conditionalise on things that only make sense for that arm. Matches get matched in order, meaning if a guard fails it falls through to check the next one, this means that guards even on data not from that arm are useful (e.g. maybe an arm only applies in certain cases, in which case other variants should take precedence). However, yes, match guards are rare.
In any case, my experience is almost all matches are simple pattern matching, there's no guards, no @s. It's theoretically possible (and occasionally occurs, sure) that someone writes a ridiculous match using its 3 separate constructs, but that's true of many things? I personally find the most annoying thing is how deep the code of a match ends up being indented.
Lastly, I hate the "X has worked fine" arguments: "mail has worked fine for for a long time, why do we need email". It feels like an intellectual shortcut to cut off discussion: if X has worked fine, it should be easy enough to defend it in comparison to the new thing (which, to be fair, you do :) ). In any case, match recognizes that switch has worked fine, and does what it can do (except fall-through, but pattern-alternation with | covers most of why fall-through is used in practice).
> Then I have to consider everything again, and I wonder why we're not just using if/else
Because it fundamentally doesn't work with enums.
Also, I feel any restriction is pretty pointless because you can always have dummy use of a value:
fn always<T>(_: &T) -> bool { true }
match x {
Enum::Variant(a) if global && always(&a) => { ... }
...
}
It's fair that people usually wouldn't do this, but still, it seems less consistent: it's special-casing the scoping rules for expressions in a match-guard, for somewhat arbitrary "code style" reasons.
> I don't think this is too bad:
That's not the impossible/difficult cases: nested matches are:
let foo: Option<AnEnum> = ...;
match foo {
Some(inner @ AnEnum::Variant(_, _ , _)) => { ... }
_ => { ... }
}
Doing this with just a 'let' requires matching each contained value of the nested pattern and then reconstructing that whole thing.
However, I agree that @ is barely useful and it's definitely rarely used.
> Sure but I guess my argument is the file/folder hierarchy approach combined with mod is a little clunky. I think it's clearer to just declare a module at the top and let people use whatever folder structure they want.
This is again something that's argued endlessly about (and I think? there's been recent work/proposals to change it).
I personally find the core pub/no-pub + mod + use system (of 1.0, I've lost track of the various additions) is nicely minimal, with those three pieces that fit together quite orthogonally/consistently. This has benefits like ease of navigation and consistent behaviour between projects, rather than the C++ style of a namespace splattered across hundreds of headers.
However, it's definitely true that it is clearly clunky and hard-to-use for a lot of people.
> Oh yeah, I was one of them. But my complaint was all the extra sigils. There was `~` for an owned pointer, `@` for GC pointers, and the `mut` suffix for mutable versions, and the worst sin of all was that `` was strictly for unsafe pointers. So the one you're most likely to recognize is the one you'll basically never see. Booooo.*
So a sigil for Box/owned pointer is okay, but not for any other library-defined types? The current behaviour is consistent: types built deeply into the language (& and &mut are the building blocks of safety, and const/mut are the building blocks of every other pointer; both of which are completely dependency-less: no allocations, etc.) get sigils, and those that are plain library types do not.
While it's fair/a little weird that raw pointers get s, I think it's fairly defensible, for a few reasons: Box is quite rare in Rust (& and &mut are used most often, when pointer-like objects are needed), raw pointers are often used when close to C so there's a sense in which not using would be "being different for no real reason", and `unsafe` code is unpleasant enough as it is to read and write using long verbose types wouldn't help. But I would also think it's defensible to not have raw pointers have sigils.
> Use _ as the default case in a match
NB. this is also consistency: _ is "match any value" in every pattern, whether as the last arm of a match or elsewhere. (And, to be clear/linking to the next point, it's always match any single value.)
> if you're destructuring in a match and you don't care about some fields, just use `..`".
While it's fair that .. is a little impenetrable, what's the alternative? Listing every field? Not writing anything at all, and having no reminder/indication that there's ignored data (and also no help with refactoring when adding fields to the type)?
---
I'm probably seeming kind-of ranty here, but I think a lot of these sort of "Rust isn't consistent/is too complicated" discussions come down to familiarity. Don't get me wrong, it's definitely unfortunate that it's unpleasant to write when one is unfamiliar (would be way better if it was smooth from the start), but there is a core consistency.
I also think it's worth separating out "Rust is complicated" and "Rust is different", although the consequence of the two probably end up being similar in a lot of cases (hard to build an accurate mental model because things are unexpected).