5 ms·
> 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 poss
by camgunz 9y ago
> 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.
100% agree, but I think it's probably worth the extra complexity in Rust's implementation to restore familiarity with casting.
I don't know if this is already a concept somewhere (I feel like it has to be) but I think that given a software or information encoding problem, there's a certain base complexity. You might be able to solve that problem in multiple different ways, splitting up the complexity in each one, but the total amount of complexity is still there.
Memory management is a good example. Memory must be managed somehow, and in languages like Python, Java, and even Rust (with lifetimes and (A)Rc) the complexity of that management is in the language/platform implementation whereas in languages like C it's in the application. Regardless, it exists.
So I would prefer that this complexity be in the implementation in order to maintain familiarity with the long history of systems and applications languages. I recognize it's more work for Rust, but as a user of Rust and not a maintainer, I'm OK with that ;)
Re: Context managers, I think we agree here and in fact, basically all I want out of a context manager is Rust's blocks. I guess a block without a statement is a little strange, but actually in Rust it's kind of idiomatic so I can get behind it.
> 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.
Woof, I do _not_ consider `return` noisy. Along the same lines as "everything is an expression is weird", I think implicit returns are weird. I guess it's maybe like everything; like when you work in Python you think braces and semicolons are annoying noise, when you work in Java you think manual memory management is annoying noise, and now maybe that goes for `return` in Rust. I'm almost never irritated by "ceremony" -- I'm pretty good at typing. Instead it's the "neat tricks" and inconsistent structure of programs that really eats my time and burns my brain cycles.
I'll admit to not being a huge fan of macros -- especially in systems languages. I think inlining functions is far less surprising, and the fact that you have reduced power compared to macros means there's far less surprising behavior (i.e. "why am I returning early..."). I know they're good for getting rid of "ceremony" but I'm guessing we'll end up disagreeing about how important that is :) But consequently I'm not really willing to give up anything to make macro writing easier.
> 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.)
Haha well, I'm not gonna defend do/while(0). Textual macros and optional braces are obviously (now) not a good idea.
Consistency is fine as long as it's good consistency. Sure C mixes a lot of statements with a few expressions, but that never bothered me because that's practically all mainstream languages. "Everything is an expression" is consistent, sure, but at what cost?
> 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.
I guess really what I want is for there to be a construct that switches between different enums, and there to be a different construct that switches between different values. Conflating the type with the value is confusing to me. Ex:
fn inspect(thing: ThingEnum) {
match thing {
case ThingEnum::ThingOne {
switch thing.thing_one_field {
case 1 {
// do ThingOne.thing_one_field == 1
}
}
}
// etc.
}
}
Yeah it's a little pyramid-y, but hey welcome to matching and variant types. If you really worked things around you wouldn't need to nest so far, but that's probably too much of a syntax change:
fn inspect(thing: ThingEnum) {
thing=>variant(ThingEnum::ThingOne) {
switch thing.thing_one_field {
case 1 {
// do ThingOne.thing_one_field == 1
}
}
}
// etc.
}
Anyway there are a lot of benefits. The distinction between switching on type and value is very clear. Blocks and control flow are very clear. It uses previously standard constructs (switch/case). I really don't need to know anything about Rust to know how this works; it is self-evident. Really maybe the only ambiguous thing is "match", which should probably be like "variant" or something, but whatever.
> 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.
100% agree.
> That's not the impossible/difficult cases: nested matches are:
let foo: Option<AnEnum> = ...;
match foo {
Some(inner @ AnEnum::Variant(_, _ , _)) => { ... }
_ => { ... }
}
While I get what this does, it looks very noisy. Losing the ability to do this in a single construct is fine w/ me if this is the result.
> ...C++ style of a namespace splattered across hundreds of headers.
That's a fair point and worth worrying about, haha. Good call.
> So a sigil for Box/owned pointer is okay, but not for any other library-defined types?
Mostly I just didn't think the (A)Rc pointers needed a sigil, and I thought it was weird that what most people would think of as a pointer used to use `~` and now uses `Box`, whereas the pointer you would (mostly) never use is the one with the most familiar sigil (`* `). Feels like that one should be `Raw` and `Box` should be `* `. Library-defined or otherwise doesn't really matter to me; and if you're building something that can't allocate, the compiler will just tell you when you can't use `alloc`/`new` or whatever. EZ.
I get what you're saying about unsafe code being closer to C though. I guess I would have made regular Rust closer to C and had unsafe be less ergonomic, as an interesting way to discourage people from using it (see Python's prolific use of `__` everywhere), so that's probably the root of our disagreement here.
> While it's fair that .. is a little impenetrable, what's the alternative?
I would get rid of struct destructuring entirely. It's only real use is inside of match, and that's packing more things into match.
> I think a lot of these sort of "Rust isn't consistent/is too complicated" discussions come down to familiarity
Oh I'll definitely cop to being 1000x better at C than I am at Rust, and the more I use it the more I'm fine with it. But moving from C to Rust (or any language to Rust) is so hard because of all of these things. I mostly work in C, Python, Java, and JavaScript and Rust is very different from all of those -- and it's hard for me to justify those differences. And as a C programmer, the borrow checker isn't responsible for the learning curve. Rather, it's all the "neat" things in Rust. All I really wanted was C with a borrow checker, or Java without GC and a 90s idea of OO (hand waving a lot here). I honestly don't see why we had to tack on all this extra stuff.
> 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).
Definitely. Point taken :)
- dbaupp 9y ago> 100% agree, but I think it's probably worth the extra complexity in Rust's implementation to restore familiarity with casting. This is framing it a trade-off between writing something complex once versus forcing everyone to handle that papercut. Which, usually, I'd agree with the going with the complex-but-only-once (a question of asymptotics, after all). However, I'm not sure it's entirely like that in this case: at the very least, this complexity here is revealed to the programmer (they have to do the same parsing switch in their head, even if it's usually fairly obvious). Additionally, this complexity applies, somewhat, to tools that work with code too, not just the compiler (e.g. limited editors trying to do syntax highlighting without running more detailed semantic analysis). This seems like such a minor thing to introduce such a heavy penalty, but maybe there's something more annoying you're finding with 'as'. > Woof, I do _not_ consider `return` noisy. Along the same lines as "everything is an expression is weird", I think implicit returns are weird. I guess it's maybe like everything; like when you work in Python you think braces and semicolons are annoying noise, when you work in Java you think manual memory management is annoying noise, and now maybe that goes for `return` in Rust. I'm almost never irritated by "ceremony" -- I'm pretty good at typing. Instead it's the "neat tricks" and inconsistent structure of programs that really eats my time and burns my brain cycles. Sure, I can see that; like any symbols, the brain quickly glazes over keywords like 'return', but it's still a little bit of processing. In any case, implicit returns do feel a bit weird to me at times, but Rust is statically typed, and the returns are never in surprising places (i.e. it's always the last thing in a function/block), which means I don't have to think about it: if it type checks, it's usually what I meant. I really do think this is just a familiarity thing: coming from languages with a strong statement vs expression distinction it's weird, coming from mathematics/languages with out the distinction, it isn't. The language isn't particularly more complex because of it, it is just slightly different. It's true that Rust's target market is mostly the former set of languages, so one could argue maintaining familiarity is critical (something Rust acknowledges: {} for scope and <> for generics driven by that), but it's also an argument that would have kept us writing slightly improved assembly languages forever. > Consistency is fine as long as it's good consistency. Sure C mixes a lot of statements with a few expressions, but that never bothered me because that's practically all mainstream languages. "Everything is an expression" is consistent, sure, but at what cost? Yes, what cost? I genuinely don't see a cost other requiring some people to get used to it, and I do see costs to the other approach. I personally hate the C/C++ pattern of having to declare things and then initialize them later. The C++ code I'm currently writing has several places where I've been forced to write things similar to: const char *name; switch (someEnum) { case X: name = "..."; break; case Y: name = "..."; break; // ... } I personally find the following to be so much nicer: let name = match someEnum { X => "...", Y => "...", // ... }; In particular, everything is together, I'm not having to skip over the low-information-density "case", "break" and "name =" to find the interesting bits (the enum variant and the string it corresponds to), plus I'm not having to what reconstruct those two statements are actually trying to do (it's just initializing name). An additional, although possibly contentious, benefit is this lets type inference work: name didn't need a type. This is more important when the type is long and complicated: type inference lets one use that complicated type without having to write it out, whereas in the declaration version, one might be tempted to go to a simpler type for development ease (or duplicate code, to not have to have 'name' live outside the switch) even if it is slower (e.g. collecting an iterator to a Vec). (I also forgot to mention ternary ?: in C: it's also partly an acknowledgement that if-as-a-expression is useful.) > I guess really what I want is for there to be a construct that switches between different enums, and there to be a different construct that switches between different values. Conflating the type with the value is confusing to me. Ex: At the very least, this makes type checking harder (both for compilers and for people trying to write/understand the code, and compiler error messages): the 'thing' variable doesn't have type 'ThingEnum', it has a changing type that starts as 'ThingEnum' but switches to some restriction of that in a branch. This opens a whole can of worms about wanting `if x is ThingEnum::ThingOne` to also restrict the type or `assert!(x is ThingEnum::ThingOne)`, and then wanting this type restriction thing to be more first class (e.g. abstracting it away behind functions, like `x.is_some()` succeeding "setting" x's type to Option::Some so that one can access the contained value). This all sounds great and useful! And, it practically all already exists naturally with Rust's (and that of most other languages with similar enums[1]) current model: the enum variant itself is a first class way to reason about the various variants, and extracting the data (or at least, binding the thing to a new variable) at the point you find out the variant means there's never a worry about making sure the compiler understands all the ways in which to decide that an enum value is actually a specific variant. I don't see much upside to emulating C-style manual tagged unions here. [1]: This is a point where other people would complain about breaking with other practice for no good reason too (anyone who had used Haskell or OCaml or similar would find this system unnecessarily clunky). Focusing on a single value also doesn't generalize/scale: for instance, if one is making a decision that depends on more than one value: fn maybeAdd(x: Option<i32>, y: Option<i32>) -> Option<i32> { match (x, y) { (Some(left), Some(right)) => Some(left + right), (Some(left), None) => Some(left), (None, Some(right)) => Some(right), (None, None) => None } } (One could, theoretically, merge the two Some/None lines, with an | pattern, I like it like the above, since the RHS is so simple. The last 3 lines of this specific example could be reduced to "(left, None) => left, (None, right) => right", but that doesn't apply generally so I didn't do it for this one.) Under a separated scheme (plus returns) the match looks like: match x { Some => match y { Some => Some(x.value + y.value), None => Some(x.value), } None => match y { Some => Some(y.value), None => None, } } Similar to the "name" example above, I find this doesn't clearly express how I think about code, and I have to reverse engineer it: I don't want to follow the tree to see "if x is some and y is some then add, otherwise [y is none] so return x, ...". That's how a computer thinks, but that's not how I want to think about my code in almost all cases: usually I want to know the task the code is doing, not the details of every little step (the latter only in cases of micro-optimisation of a tight loop/hot function: if necessary, I can still write the Rust in that form). The Rust more declaratively expresses "if they both exist, add the values, otherwise if one of them exists, return that, otherwise return nothing". Also, slightly related, but there has been semi-regular discussions around allowing an individual enum variant to be treated as a struct, essentially, so that instead of needing to define a whole new struct for any variant that might need to be manipulated on its own, one can refer to the variant directly: struct Foo { x: i32, ... } enum Bar { Foo(Foo), ... } enum Baz { Foo { x: i32, ... } ... } Then Baz::Foo would be a struct-like type. (However, any proposal here would still require a match and new variable bindings and so on, because that avoids all the problems of having things have variable types.) (Continued...)