5 ms·
> 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
by 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...)
- dbaupp 9y ago(...) > 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. FWIW, I agree, just demonstrating that replacing @ in patterns isn't the easy case as it is with the classic integer range example you gave. :) > 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.* The one I think of as a pointer is & and &mut. And, there's a whole pile of reasons that Box doesn't get used nearly as much as * and malloc in C: - arrays and non-owning pointers use different syntax (Vec/[] and &/&mut respectively) - proper generics means much fewer places where one needs to create a void* to pass data around (e.g. std::thread::spawn vs. pthread_create) - enums means polymorphism and optional-ness can be done with "inline" types with little ceremony. But yes, as I said, it is a little weird that the dangerous pointers gets relatively nice syntax. > 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. If you're including tuples as structs, I strongly disagree: it's great for `let` and multiple returns (and even true structs, not tuples, are sometimes nice there, although 'foo'/'bar' has less benefit versus a struct's 'x.foo'/'x.bar' than versus a tuple's 'fooAndBar.0'/'fooAndBar.1'). Also, .. works for struct enum variants: the Baz::Foo variant above is valid Rust syntax now. > 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. Yeah, it's definitely true that new things have a "change budget": how different they can be before it's too much. Rust has been designed with this in mind (things like {} and <>, as I mentioned above), but different people's threshold for it are different. There's lots of changes in Rust over C that make code more declarative and require less mental reconstruction (for me), which means I enjoy writing Rust more than if it didn't have them, but it's definitely true that they aren't literally necessary, or that they could maybe be phrased in more restricted ways that match closer to C. I've personally found being exposed to many very different languages has helped inform the code I write in all others. Each new one definitely takes some getting used to, but my experience is that having touched several different paradigms has made me both more flexible (less attached to any particularly way of doing things) and a deeper understanding of even the "boring" languages: the trade-offs and "whys". There's value to things being the same, but there's also value in being able to break out of that mold and doing new things even if there's not an obvious benefit when focused on the old style. I hope that you find Rust more and more enjoyable as you use it more, and if not, that's unfortunate: not everything works for everyone. Hopefully, at the very least, Rust inspires other languages that suit you better. :)