[HN Gopher] Rust Weird Expressions
___________________________________________________________________
Rust Weird Expressions
Author : linkdd
Score : 213 points
Date : 2021-05-14 11:01 UTC (11 hours ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| krick wrote:
| Yeah... I didn't want to admit that for a really long time, but
| Rust isn't really a huge success. You can obfuscate the code in
| most (all?) of the languages, but the fact that all of these
| things are even possible, definitely isn't something to be proud
| of.
| mjbrusso wrote:
| Wouldn't it be time to create a International Obfuscated Rust
| Code Contest (IORCC)?
| steveklabnik wrote:
| We ran something similar in 2016: https://blog.rust-
| lang.org/2016/12/15/Underhanded-Rust.html
| smoldesu wrote:
| I like the idea, but I think Rust is too picky of a language to
| use for something like that. IOCCC is fun because of how
| interesting it is to watch seasoned C professionals finesse
| their favorite language. A Rust variant would likely end in
| tears, copious compiler warnings and perhaps blood.
| linkdd wrote:
| Isn't it just a normal Rust program ?</troll>
| skohan wrote:
| I have no idea what I am reading
| phaylon wrote:
| It's the part of the Rust compiler test suite that makes sure
| odd syntactical combinations still behave as they are supposed
| to.
| chrismorgan wrote:
| And I guess it's 2015 edition, because it'll fail in 2018
| edition Rust: where the 2015 edition has ::u8(0u8), the 2018
| edition will need you to write crate::u8(0u8) or
| self::u8(0u8), since only crates exist at the top level now,
| rather than the contents of the current crate as well.
| est31 wrote:
| Yeah it's indeed the 2015 edition because while compiletest
| watches for _/ / edition:something_ comments [0] (like this
| [1]), it doesn't pass any edition flag to the compiler if
| no such comment is present, and the rust compiler defaults
| to the 2015 edition if none was specified.
|
| [0]: https://github.com/rust-
| lang/rust/blob/69b352ef7749825abde2d...
|
| [1]: https://github.com/rust-
| lang/rust/blob/69b352ef7749825abde2d...
| Macha wrote:
| However, the compiler has to continue to be able to compile
| 2015 edition code, as it is still guaranteed to work and be
| intermixable with 2018 (and soon, 2021) code in the same
| application.
| chrismorgan wrote:
| For curious people unfamiliar with the situation with
| this: Rust cares about backwards-compatibility so that
| you should still be able to compile code from 2015 in
| 2042 (some libraries are just stable!), but it also wants
| to allow certain changes to the language that you might
| think would be backwards-incompatible. Each crate
| (library) declares an edition, and the compiler follows
| the rules of that edition, and then you can link all the
| different crates together without worrying what edition
| they were written in. Editions are limited in what they
| can change, because things like traits are shared between
| crates, so they can't be changed in editions; it's mostly
| syntactic stuff, like the 2018 edition making async/await
| keywords and making dyn a full (rather than conditional)
| keyword.
| fat-chunk wrote:
| I'm not very knowledgeable with how compilers or language
| standards work, but would there not be security
| implications with this approach?
|
| For example let's say a security exploit surfaces in the
| 2015 edition of Rust, would that not mean all the
| libraries declared as 2015 edition would have to be
| updated or abandoned in that case?
|
| Or now that I think about it, is it instead the case that
| a whole program including all dependencies will be
| compiled by the same compiler (of which newer editions
| will have the latest security fixes), just that the
| compiler will always have to support compiling programs
| using legacy syntax when it identifies the crate's
| edition?
| rabidferret wrote:
| > Or now that I think about it, is it instead the case
| that a whole program including all dependencies will be
| compiled by the same compiler (of which newer editions
| will have the latest security fixes)
|
| It's this. Rust doesn't (yet) have a stable ABI for
| functions that aren't marked `extern "C"`. Any security
| vulnerability that would affect code in rust-lang/rust
| would most likely be in the standard library, which
| doesn't change between editions. All code links to the
| same libstd. Only the compiler frontend changes
| greenshackle2 wrote:
| It's just syntax differences. The newer compiler supports
| all previous language editions, you're not using a
| 2015-era compiler to compile 2015 edition code.
|
| Rust is not ABI-stable, there is no guarantee that you
| can even mix libs built with different versions of the
| compiler. The entire Rust tooling is built around static
| linking and building all your dependencies from sources.
| So yes, all the crates that go into your program are
| built with the same compiler, it's just that the compiler
| knows how to paper over the syntax differences in the
| different language editions.
| peterkelly wrote:
| This is what Python should have done.
| winstonewert wrote:
| This approach only works for syntax level changes, it
| wouldn't have helped the problematic changes in Python 2
| -> 3.
| cyphar wrote:
| Well, the ability to specify python version by module
| would've made migration much easier for everyone (in
| theory). But you're quite right that it wouldn't by
| itself be a solution -- it would also have required
| additional complexity to handle interoperability when
| calling between python versions, both in the runtime and
| the programs themselves (even a sufficiently smart
| compiler can't figure out what string encoding a python2
| function expects).
| egeozcan wrote:
| JavaScript had something close, valid on the context
| level, with "use strict" (which is, I guess, borrowed
| from Perl), and I still don't understand why they don't
| repeat that for newer features that would be much simpler
| if they broke backwards compatibility.
| iudqnolq wrote:
| There's even a handy macro for switching between them /s
|
| https://twitter.com/m_ou_se/status/1392200805168689154
| phaylon wrote:
| Yeah. I'd assume a 2018 specific one would show up below
| https://github.com/rust-
| lang/rust/tree/master/src/test/ui/ru... if needed.
| berkes wrote:
| Me neither. But I'd guess it's to test (or explain, or
| document) edge-cases or unexpected behaviour when using Rust
| std.
| Minor49er wrote:
| I've seen those sorts of things in other languages, though
| they're often accompanied by explanatory comments, if not a
| separate document. Given that these appear to be in a test
| suite for Rust itself, I'm surprised that these aren't
| explained in the code (at least as far as I saw)
| iudqnolq wrote:
| I'd like to nominate anything by Mara Bos (@m_ou_se) on Twitter.
| For example:
|
| > What's your favourite @rustlang boolean? Option<()>,
| Option<Option<!>>, std::iter::Once<()>, std::fmt::Result
|
| > tired: u16, wired: BTreeSet<BTreeSet<BTreeSet<BTreeSet<()>>>>
|
| > What's your favourite @rustlang integer? &[()],
| Enumerate<Repeat<()>>, Poll<File>, *const PhantomData<usize>
|
| @jaaaarwr> How is Vec<()> not in this poll?
|
| > That's just the owning version of &[()], right? Why would you
| need to own ()s? ;)
|
| Edit: She's also the author of inline_python!, a serious macro
| for interspersing code that calls python (including local
| variable interop), and whichever_compiles!, a joke macro that
| takes multiple expressions, forks the compiler (as in a fork
| syscall), and compiles the first one that compiles to valid rust.
| estebank wrote:
| Result<(), ()> is just a spicy bool.
|
| A lot of people are on board with not using if/else and instead
| :) match x { true => {}
| false => {} }
| skrtskrt wrote:
| > Result<(), ()> is just a spicy bool.
|
| EDIT: quoted wrong part of parent, ignore above
|
| > match x { true => {} false => {} }
|
| I really like this. Pattern matching syntax is great, nice to
| use in place of if/else as much as possible
| OJFord wrote:
| Hang on aren't we conflating two things in this thread? Or
| you quoted the wrong half of GP's comment perhaps? You can
| pattern match on an actual `bool`; if you use `Result<(),
| ()>` instead you'll have to match on `Ok(_)`/`Err(_)`,
| which is bad because one of your Boolean values is an
| 'error' now, and also because which one is is up to you (to
| remember)!
| iudqnolq wrote:
| I disagree. There's a clippy lint against pattern matching
| on bool. I also think Result has the connotation of
| "success or error". Plus it's always annoying when a
| function returns an error that doesn't implement Error and
| I have to wrap/replace it to make it integrate with the
| standard error handling ecosystem.
|
| There's a nice alternative though, which is empty struct
| errors. #[derive(Debug, thiserror::Error,
| displays of::Display)] /// Frobnicating the foo
| failed. struct FrobError;
|
| The advantage is the standard advantages types use, i.e. if
| I want to propagate it I know this specific zero sized type
| has a specific meaning. It'll also show a nice message if
| you print it.
| estebank wrote:
| I like matching on bools on the same cases where someone
| wouldn't write braces around the body in C: whenever the
| corresponding expressions keep you under the line length.
| For example: let plural = match
| cases.len() > 1 { true => "s",
| false => "", };
|
| Of course, you'd more likely have something like
| let prefix = match cases.len() { 0 => "no
| values".to_string(), 1 => "a
| value".to_string(), n => format!("{} values",
| n), };
| rabidferret wrote:
| let plural = match cases.len() { 1.. => "s",
| 0 => "", };
|
| ;P
| cyphar wrote:
| That's not the correct plural rule in English, you'd want
| let plural = match cases.len() { 0 | 2.. =>
| "s", 1 => "", };
|
| or something (though to be fair the original code snippet
| also handled "0" incorrectly -- should be "cases.len() !=
| 1").
| iudqnolq wrote:
| And also to be fair you probably don't want to be
| hardcoding English pluralization rules.
| spullara wrote:
| My favorite thing about internationalization is that some
| languages have 6 different translations for different
| amounts.
| iudqnolq wrote:
| Interesting. I prefer writing that as an if/else on one
| line when rufmt lets me, but I see your point.
| Tuna-Fish wrote:
| > whichever_compiles!, a joke macro that takes multiple
| expressions, forks the compiler (as in a fork syscall), and
| compiles the first one that compiles to valid rust.
|
| This is inspired.
| iudqnolq wrote:
| It's definitely worth reading the full thread if you haven't
|
| > Did you know you can just fork() the @rustlang compiler
| from your proc_macro? :D
|
| > This is, of course, a horrible idea.
|
| > Unfortunately rustc is multithreaded and fork() only forks
| one thread. We'll just assume the other threads weren't doing
| anything important.
|
| > In today's rustc, this means we're only missing the main
| thread in the fork. But that one was only going to call
| exit() after joining our thread anyway, so not very
| important. It just means the process always exits with a
| success status. So instead we grep the output for "error".
|
| Any my favorite reply:
|
| > Is this a rust implementation of autoconf?
|
| https://mobile.twitter.com/m_ou_se/status/136863270144881869.
| ..
| tux3 wrote:
| Another fun one (and an easter egg): fn main()
| { break rust; }
|
| This breaks rust =)
| linkdd wrote:
| My favorite is the punch card example.
| Lvl999Noob wrote:
| How does that one work?
| steveklabnik wrote:
| fn punch_card() -> impl std::fmt::Debug {
|
| This is a function that returns some type that implements
| Debug. Not specifically named.
|
| The rest of it is a combination of two syntaxes: .. and ..=
|
| These are both ranges. The former exclusive, the latter
| inclusive. You can write something like 1..5 (or 1..=5) to
| get a range from 1 to 5 inclusive/exclusive, but you can
| also leave the start or end off: 2.. will give you from 1
| till the end, ..=5 will give you from the start to 4. This
| means that .. will give you the full range. It looks more
| normal when used in context: fn main() {
| let s = "some string"; let some = &s[..=4]; //
| you'd probably write ..5 but i wanted to show both syntaxes
| let string = &s[5..]; let all = &s[..]; // a
| no-op here but useful if s were a String and you wanted
| 'all' to be a &str println!("{}",
| some); println!("{}", string); }
|
| prints "some" and then "string".
|
| So, here's the tricky bit: you can overload what the range
| is over. They're not just for numbers. So:
| fn main() { let mut string = 'a'..='z';
| let a = string.next(); let b = string.next();
| println!("{:?}", a); println!("{:?}", b);
| }
|
| prints "Some('a')" and then "Some('b')", because iterators
| return Options.
|
| So... all of this means that, you can implement a range of
| ranges. And so this string builds up a ridiculous unbounded
| range of unbounded range of unbounded range of ...
| lmkg wrote:
| I'm shocked that this doesn't have my own personal favorite:
| fn eject() -> i32 { return return return return return
| return!!!!!!!!11; }
| woah wrote:
| What is this?
| pitaj wrote:
| `!` is binary negation for integers. `return x` is an
| expression that can be used in place of any type because it
| will never be used.
| masklinn wrote:
| > `return x` is an expression that can be used in place of
| any type because it will never be used.
|
| To clarify: the expression will run, but it has the type
| `!` ("never"). And while that's still not a "proper" rust
| type (other uninhabited types are though they may not be
| quite as special-cased in the compiler), it is in essence a
| bottom type: since it can't have a value, it will unify
| with any type.
|
| That's why Rust is perfectly happy with
| let foo = if a { 1 } else { return 42 };
|
| The first branch has type `{integer}`, the second branch
| has type `!`. `!` unifies just fine with `{integer}`,
| therefore the typechecker is happy.
| OJFord wrote:
| Whaat, was this new in 2018 ed? I could've sworn it was
| `()` and so you couldn't do that. I don't write rust that
| often, and happen to be reading this just as I've written
| some code that's.. not that messy but not that I'm
| pleased with, wrangling around exactly this sort of if-
| assign-else-return pattern.
| orthoxerox wrote:
| () is a unit type, it has a single possible value. ! has
| _no_ possible values.
| steveklabnik wrote:
| I don't believe it should be new, but this can happen by
| accident sometimes. Like, if you included a ; on the non-
| return branch it would return () overall and you probably
| thought the return did it when it wasn't that. Hard to
| say!
| caturopath wrote:
| What's weird about `fn evil_lincoln() { let _evil =
| println!("lincoln"); }`?
| steveklabnik wrote:
| https://news.ycombinator.com/item?id=27154557
| caturopath wrote:
| Thanks a bunch!
| rabidferret wrote:
| I have always wondered this!
| steveklabnik wrote:
| Four years ago I wrote about my favorite example in this file:
| https://news.ycombinator.com/item?id=15495027
|
| Some things only make sense if you know very old Rust, and
| they've just been updated as the language has been updated,
| obfuscating their original meaning.
| bruce343434 wrote:
| So, semantically, what is `f(return)`? Are these examples
| supposed to actually do anything, or mirror actual code
| situations? Or is it just putting a monkey in front of a
| typewriter?
| chrismorgan wrote:
| In Rust, just about everything's an expression. `return` is an
| expression that diverges, meaning it's of type _never_ (spelled
| !, and coercible to any type, since you know the value will
| never actually be used).
|
| Here are another couple of ways of writing the same general
| concept: f({ return; () //
| () in this case because f takes a parameter of that type
| }); let x = return; f(x);
|
| So f() will never actually be called. You'll get an
| unreachable_code warning on f.
|
| As a minimal case it's mostly just funny, but the underlying
| _concept_ does find its way into real Rust code, mostly in
| generated code (such as with macros). Also, although just about
| everything's supposed to be an expression, sometimes things
| that can be an expression or a statement go a little bit funny,
| and return is a prime candidate for things going wrong in weird
| ways in the compiler frontend or in code generation or
| something, so it's worthwhile having such tests (there will be
| more involved tests of return specifically elsewhere).
| brundolf wrote:
| I always thought return, like assignment, could only be a
| statement and not an expression
| steveklabnik wrote:
| https://doc.rust-
| lang.org/stable/reference/expressions/retur...
|
| They're always expressions in Rust, though they are often
| written with ; to make an expression statement.
| chrismorgan wrote:
| See also https://doc.rust-
| lang.org/stable/reference/statements.html on what
| statements are.
|
| Basically, the only thing that is a statement and can't
| be an expression is `let _pattern_ = _expression_ ;`.
| estebank wrote:
| And even _that_ will likely change in the future, letting
| you write things like if some_bool &&
| let pat = expr {}
| skohan wrote:
| So does this mean if I call `f(return)`, it will return from
| the calling context without ever calling the function `f`?
| ben0x539 wrote:
| yes, it's roughly comparable to {
| let x = return; f(x) }
|
| where the call to f is clearly happening after an
| unconditional return.
|
| A _slightly_ more practical example might be something like
| f(if blah() { 42 } else { return })
|
| which works out to, roughly, {
| let x; if blah() { x = 42;
| } else { return; }
| f(x) }
|
| To get realistic, if you write f(g()?)
|
| you basically get f(match g() {
| Ok(x) => x, Err(e) => return Err(e), })
|
| which is how rust does error propagation without exceptions
| and without constantly repeating a golang-style mantra.
| chrismorgan wrote:
| Building on the more practical example, this sort of
| conditional early return is extremely common in Rust,
| because expression-orientation makes it very pleasant to
| use. Here's a sketch of some of the sorts of ways it can
| happen in state machine: loop {
| // ... state = match state { A =>
| B, // normal B => break, // exits the
| loop C => { ...; continue; }, // does
| something and then returns to the start of the loop,
| skipping setting the state and anything else
| D => return, // exits the whole thing E =>
| f()?, // if f() returns an Err or None or similar, it'll
| return early }; // ... }
| tanaypingalkar wrote:
| i think this = #[]; is for rust analyser, not sure , can anyone
| tell me what is this #[];
| steveklabnik wrote:
| It is called an "attribute" and it is part of the language
| itself.
|
| There is also #![]. The difference is what they apply to, #[]
| applies to the following thing, #![] applies to the parent
| thing. You'll see #![] to enable nightly features in Rust, for
| example, and #[] for things like custom derives.
| Zababa wrote:
| I'd like to add the "Bastion of the Turbofish", which is my
| personal favorite: https://github.com/rust-
| lang/rust/blob/master/src/test/ui/ba...
| iudqnolq wrote:
| To explain this, since it took me a minute:
|
| Rust has a syntax called "turbofish" for disambiguating
| ambiguous generic calls. (Coined by Anna Harren on twitter, the
| first reply is an illustration from Karen Rustad Tolva, the
| inventor of Ferris [1]).
|
| Although Vec is generic over any item type, you can normally
| just write let foo = Vec::new();
|
| If you're doing something complicated where the compiler can't
| infer the type, you instead use the turbofish syntax
| let foo = Vec::<u32>::new();
|
| Some people hate this syntax. One of the better arguments I've
| seen against it comes (I think) from ManishEarth, who points
| out that it's very hard for the compiler to detect that
| incorrect attempts at writing a turbofish are in fact attempts
| at the turbofish and offer help.
|
| That test case (minus some excellent poetry) is
| let (oh, woe, is, me) = ("the", "Turbofish", "remains",
| "undefeated"); let _: (bool, bool) = (oh<woe, is>(me));
|
| That is actually comparing the string "the" against the string
| "Turbofish" and the string "remains" against the string
| "undefeated". The test case is there to point out that if the
| "::" in the turbofish is removed (Vec<u32>::new() instead of
| Vec::<u32>::new()) there will be cases where it's ambiguous.
|
| [1]
| https://twitter.com/whoisaldeka/status/914914008225816576?la...
|
| EDIT: Fix my misspelling of ManishEarth
| IainIreland wrote:
| For the record, I suspect "Manis Heath" is in fact
| "ManishEarth", aka Manish Goregaokar.
| iudqnolq wrote:
| Thank you! Fixed
| OJFord wrote:
| If it helps, for _years_ I read 'dang' as just a fun
| internet alias, as in Dang son, that's embarrassing.
|
| It wasn't until that NYT (or whichever paper) interview
| with 'Dan G[...] HN moderator' that it clicked.
|
| (I still frequently hear it wrong in my head. Habits..)
| btown wrote:
| Oh gosh - I was so primed by the poem to see <woe, is> as a
| generic specifier (which I thought should fail, because "oh"
| isn't callable)... that I completely forgot that < is a less
| than operator and > is a greater than operator. This
| literally compiles as two string comparisons. I need to go
| home and rethink my life.
| steveklabnik wrote:
| The grammar conspires against your brain; when used as
| comparisons, we tend to include spaces around them, and as
| generics, we don't. But it's not _required_ for either of
| those to be that way, with the whitespace. Tricky!
| eslaught wrote:
| See also this comment that makes the ambiguity explicit:
|
| https://github.com/rust-
| lang/rfcs/pull/2527#issuecomment-414...
| galangalalgol wrote:
| I really want to like rust but it seems so very complicated. C++
| is too but I have been growing with it. It has been my primary
| language for 27 years.
| linkdd wrote:
| While I agree that Rust is complicated, this example from the
| test suite is really a bad example because it's there to test
| the edge cases of the grammar to ensure backward compatibility.
| mbStavola wrote:
| Pick any language and I'm sure someone can point you to a test
| suite like this, it's not really indicative of a complexity
| problem in Rust.
| kortex wrote:
| I learned in this order: C, "C++" (very C-like, just enough to
| write arduino stuff), rust, and the "real" C++14.
|
| Unless you write C++ like C with classes, which is not really
| writing C++ at all, Rust is actually simpler, easier to learn
| on your own, more consistent, with better compiler error
| messages, tooling, documentation, etc.
|
| Your situation - boiling frog analogy - is a very common case
| but also the only one in which case C++ feels less complicated,
| simply because it's familiar.
|
| Everything about C++ feels overwhelming, with so many decisions
| and degrees of freedom at every step, from build system, to
| lifetime management, mutable state, concurrency, and threading,
| to libraries, to dependencies, to deployment. It often feels
| easier, especially for beginners, because there are so many
| ways to silently do the wrong or suboptimal thing.
| rkangel wrote:
| One thing that people say is that "Rust frontloads your
| problems". C++ might be a bit easier to get your head around to
| start with, but as the complexity of what you're building grows
| you will discover the 'joy' of debugging segfaults and race-
| conditions.
|
| Rust has some type-system complexity to get your head around.
| It's not as painful as it looks (when you're just reading it
| rather than trying to do it) but there is a hump of
| understanding there that you have to get over. Once you've done
| that though, Rust then helps you so much more in dealing with
| the 'long tail' of difficult problems.
|
| Another way of looking at it is that there are a load of things
| that you can silently get wrong in C++. There are good
| practices that help you not screw up e.g. you have to learn how
| to think about who has pointers to what and how long they live.
| Rust _forces_ you to think about that stuff from day 1 - the
| compiler requires you to prove to it that everything is good.
| It 's a bit painful to start with, but it's a good thing
| overall.
| smoldesu wrote:
| If you've been working with C++ for over 27 years, you owe it
| to yourself to at least try Rust. A lot of the features in it
| are designed to ergonomically operate around the limitations of
| compiled languages, while also encouraging the developer to
| write "correct" code. If you've got some spare time, I
| wholeheartedly encourage giving it a try!
| pornel wrote:
| This is not the same kind of complexity. Rust mostly reflects
| complexity of its domain (thread-safe, memory-safe, low-level
| without runtime, a lot of correctness enforced at compile-
| time), rather than unfixable baggage of C and C++'s early
| design decisions.
|
| I'm not saying all the complexity of C++ is its own fault, but
| Rust shows how much of it is unnecessary. It manages without
| any constructors at all (think how many rules and features are
| connected to them!), without inheritance, and with only _one_
| (1!) way to initialize a variable.
| larschdk wrote:
| I often feel the same, and there is certainly a bit of learning
| curve, but in my experience, all the time you spend battling
| the compiler in Rust is time saved from debugging in C++,
| several times over. I'm repeatedly surprised by how my code
| just works when it finally compiles. It's like the compiler is
| telling you, "you just made a bug".
| ncmncm wrote:
| If you _ever_ find yourself spending time "debugging in
| C++", you are Doing It Wrong. Coding modern C++ right is the
| same, that way, as coding Rust: when it compiles, it works.
|
| (In general, if you find yourself inventing falsehoods about
| other languages to promote Rust, you are Doing That Wrong,
| too.)
|
| I have personally spent more time, summed over the past
| decade, preparing bug reports against Gcc than in debugging
| C++ memory-usage faults.
|
| But C++ compiles faster.
|
| Coding Rust is fun in much the way that Forth is: it is a
| charge to figure out a way to achieve a thing; you know it
| will ultimately be possible, and when you find the way, it is
| an ego boost. You could say, "Rust is the Second Programming
| Religion", and who could ever _honestly_ disagree?
|
| (But watch extremists downvote this to oblivion anyway.)
| nix0n wrote:
| > Coding modern C++ right[...]: when it compiles, it works
|
| I don't believe this, but I'd love to be wrong. Is there an
| exemplary codebase somewhere that I can take a look at?
| shakow wrote:
| I can give you an example I used a few months ago:
| struct Shape { Shape() { init(); };
| void init() { reset(); }; virtual void reset()
| = 0; }; struct Point: Shape {
| virtual void reset() { _x = _y = 0; } double
| _x, _y; }; int main() {
| Point p; // KA-BOOM }
|
| Compiles without any warning with -Wall & -Wpedantic,
| fails at run.
| ncmncm wrote:
| Thank you, this is an example of '90s-style C++, serving
| as a nice contrast to modern C++. Nowadays we would write
| struct Point { double x{}, y{}; // zero-
| initialization }; int main() {
| Point p; // ok, {0.0, 0.0} ... p =
| Point{}; // no need for a "reset" }
|
| It has been literal decades since anyone competent would
| have made a Point derived with virtuals, or have written
| so much code to achieve so little. (You might see stuff
| like that at Google.) You can write bad code in any
| language, but if you have to do extra work to make bad
| code, it is not tempting.
| shakow wrote:
| > if you have to do extra work
|
| I would argue that (i) old-fashioned code is not
| deliberately going out of your way to write bad code, and
| (ii) this should, at the very least, trigger a warning
| from the compiler.
| ncmncm wrote:
| Old-fashioned code is not necessarily _bad code_ ,
| although the example was; but the topic was "modern C++",
| not "old-fashioned C++".
|
| Anytime you do all the extra work to write old-fashioned
| code, you have earned the outcome you get. The oldest-
| fashioned code looks just like C, which you _can_ still
| write in a C++ program, _if you want to_. But there is no
| reasonable temptation to. Good modern code is equally
| fast, often faster, and more easily written, understood,
| and maintained.
| kelnos wrote:
| I haven't done any serious C++ in about 15 years.
|
| If I were to try to pick it up again today, I would have
| to learn "modern C++"[0] incrementally. I would still
| have some old habits, and they would take time to iron
| out. My guess is that it would take me at least a year,
| possibly more, to become fully proficient in "modern
| C++".
|
| And even then, I'd still expect that I'd occasionally
| write some C++ in the "old" way. Maybe I'm tired and
| forget, maybe I'm lazy and want a shortcut. Who knows.
| The compiler won't save me from my "old C++". It'll be
| there, warts and footguns and all.
|
| I'd much rather write in a language designed to not have
| these problems in the first place, and let the compiler
| catch as many problems as it can.
|
| [0] Whatever "modern C++" means; I suspect current C++
| developers can reasonably disagree on the details, as has
| been the case for the entire history of C++.
| linkdd wrote:
| Modern C++ is what was in the Boost framework 15 years
| ago.</troll>
|
| Joke aside, the STL grew in complexity and features to
| provide more compile-time constructs and to integrate
| more functional programming aspects.
|
| For example: I've been told many times that in "modern
| C++" you don't need `new` nor `delete`.
|
| There are smart pointers, std::array, std::optional,
| const-expressions, you'll find more and more "single-
| header" libraries (for JSON, an either monad, etc...),
| even modules[1].
|
| It's like learning a new language. C++11, 14, 17 and 20
| are completely different to C++03 while remaining
| backward compatible (a critical feature for C/C++
| languages it seems).
|
| [1] - https://en.cppreference.com/w/cpp/language/modules
| shakow wrote:
| > but the topic was "modern C++", not "old-fashioned
| C++".
|
| The problem being that there is no definition of "modern"
| C++, and even less so that would be enforced by
| compilers.
|
| > Anytime you do all the extra work
|
| There is no extra work to write "old-fashioned" code.
| Quite the opposite actually; one has to indicate to
| compilers to accept newer features rather than the
| opposite.
|
| > Good modern code
|
| This is a very flimsy, handy-wavy concept, that changes
| drastically from one "best practice" guide to the other.
| nix0n wrote:
| You've misunderstood me, "ncmncm" has claimed that a
| "right" way to code C++ exists. I would like to see such
| a codebase, if one exists.
|
| Clearly, this isn't it, but I do appreciate the
| interesting snippet. Would the compiler have caught this
| if `Shape()` called `reset()` directly?
| kortex wrote:
| > Coding modern C++ right is the same, that way, as coding
| Rust: when it compiles, it works.
|
| Works in what way? Serious question. Do you mean "works" as
| in 1) "doesn't crash/seg/overflow" or 2) "doesn't UB" or 3)
| "doesn't have race bugs" or 4) "does what the programmer
| intended"?
|
| I would say from my experience, none are strictly true.
| I'll hazard you mean to imply the dev knows the language
| pretty well in order to satisfy "works lvl 1" but you still
| have veterans running into bugs of the 2-4 variety.
|
| But even to get to (1), C++ requires a _ton_ of domain
| knowledge. I 've been working with C++ intensively for
| several months and incidentally for years, and I still seg
| every now and then. Even after using Valgrind, it _feels_
| rickety. Meanwhile, I 've spent a few dozen weekends on
| Rust and I just feel way more confidence that my program
| will "just work" without crashing.
|
| That's all still "level 1 works". When it comes to race
| conditions and programmer intent making its way into
| correct code, it's no comparison. Rust is way easier to get
| my intent into a running program. C++, I still have to lean
| into logs and debugging, because it just doesn't quite do
| what I want a non-trivial amount of the time.
|
| It's not a religion. Rust simply is a better experience. I
| can say that having learned both basically side-by-side.
| skohan wrote:
| > it is a charge to figure out a way to achieve a thing;
| you know it will ultimately be possible, and when you find
| the way, it is an ego boost.
|
| This is actually something I agree with, even as a fan of
| Rust. Sometimes I wonder to what extent the appeal of Rust
| is based in the fact that you get to feel like a CS
| undergrad again, climbing mountains to make code compile
| and run
| emtel wrote:
| The only way this is even close to true, imo, is you have
| really good tests and have taken the time to get those
| tests running with every available sanitizer, including
| MemorySanitizer (which is a huge pain).
|
| And even then, I think you're still behind rust. Sanitizers
| can only catch data races / memory errors / uninitialized
| reads / etc that actually happen. OTOH, Rust _proves_ that
| your program doesn't have these faults (modulo unsafe
| blocks).
| ncmncm wrote:
| If you are relying on testing for correctness, you have
| already lost. In any language.
|
| As Dijkstra noted, testing can only prove a program
| wrong. The way to get correct programs, in any
| expressive-enough language, is by construction. At each
| level, do only operations that are well-defined by the
| level below. Expose only well-defined operations to the
| next level up. The compiler proves that the types are
| used correctly; so, when coding a library, you put the
| type system to work to make wrong code harder than
| correct code.
|
| That is the right way to code Rust, too.
| linkdd wrote:
| It was my understanding that the memory model of Rust is
| inherently different than C++'s.
|
| Genuine noob question: can you really compare Rust's borrow
| checker and lifetime management to the smart pointers in
| C++?
| steveklabnik wrote:
| You can compare them, but the comparison won't show that
| they're equal :p
|
| C++'s smart pointers have direct analogues in Rust,
| though there are some differences. (unique_ptr<T> and
| Box<T>, shared_ptr<T> and Rc<T>/Arc<T>)
|
| The borrow checker is something else completely. The
| closest analogue in C++ is the https://isocpp.github.io/C
| ppCoreGuidelines/CppCoreGuidelines, which offer _some_
| similar kinds of checks to Rust, but don 't attempt to go
| nearly as far as Rust does.
| ncmncm wrote:
| There is nothing similar between Rust's borrow checker
| and anything in C++.
|
| In modern C++ you get compile-time correctness by
| construction: you do things that reduce to correct code.
| All of the low-level mistakes remain possible, but are
| not tempting. Low-level, C-like operations are like
| writing "unsafe" in Rust; you can if you want to, or need
| to, but they are uglier, and anyway almost always not
| needed. Just as when coding a Rust "unsafe" block: if it
| ever is, you use extra care.
|
| In a very real sense, C++ libraries perform in the role
| of the Rust compiler by insulating you from operations
| that are risky. The operations a good library exposes are
| safe. A Rust compiler bug could expose you to a memory
| fault, in the same way that a C++ library bug could; but
| the Rust compiler is well exercised and tested, much as
| is is a mature library. You need libraries anyway.
| galangalalgol wrote:
| Thats fine until "that guy" grabs the raw pointer out of
| a smart pointer and deletes it, or c style casts an
| interface to his version of the interface because he
| wanted it to return an int instead of a double. Then
| either pushes straight to dev or rounds up similarly evil
| individuals to approve the merge. I have debugged those
| errors. You can talk about accountability but even if you
| manage to get rid of one such dev there always seem to be
| more. And modern C++ doesn't really have great tools for
| thread safety. Its easier to get right than in 98, but
| still not hard to get wrong.
| estebank wrote:
| Genuine question, is iterator invalidation still a
| problem that people can hit with modern C++ constructs? A
| quick search seems to answer "yes"[1], but I'm not
| involved in writing C++ to evaluate what the common
| mitigations are.
|
| For what is worth, I believe that 99% of what you can
| express in one language you can in the other. If you're
| hitting walls with the borrow checker when writing Rust
| you always have the options of either using the
| equivalents of unique_ptr<T> and shared_ptr<T>, or of
| cloning memory.
|
| > A Rust compiler bug could expose you to a memory fault,
| in the same way that a C++ library bug could; but the
| compiler is well exercised and tested, much as is is a
| mature library.
|
| I'm not sure what this sentence was expressing, but it
| seems you're implying here that rustc isn't well
| exercised and tested?
|
| [1]: https://medium.com/@lightcone/iterator-invalidation-
| in-moder...
| ncmncm wrote:
| Iterator invalidation, like pointer invalidation, remains
| a thing. Iterators and pointers are normally treated as
| ephemeral, except where guarantees are provided.
|
| It is hard for me to imagine how " _the compiler is well
| exercised and tested_ " could be perceived to mean the
| opposite. Explain?
| estebank wrote:
| > It is hard for me to imagine how "the compiler is well
| exercised and tested" could be perceived to mean the
| opposite. Explain?
|
| I misread it as talking about modern c++ compilers and
| libraries being well exercised and tested, and
| _contrasting that_ with rustc.
| kelnos wrote:
| I think you're fundamentally misunderstanding the
| objections people have to "modern C++" when compared to
| Rust.
|
| You say that it's not "tempting" to use less-safe
| constructs in C++ these days, but that's not the point.
| You are assuming a perfect C++ developer who never makes
| mistakes and always knows to use the proper, modern
| constructs, and will properly document any time that they
| absolutely must use a less-safe construct, and that
| documentation will never drift or go out of date.
|
| That developer does not exist in any meaningful or useful
| way. Developers make mistakes. Developers don't always
| have completely up-to-date knowledge or understanding of
| what the correct, "modern C++" way is to do literally
| everything. A developer who genuinely does need to do
| something less safe may or may not document it, and
| anyone who comes by later and changes the code may not
| update the documentation accordingly.
|
| The Rust compiler will not allow you to do unsafe things
| (absent bugs in rustc); attempting to do so is a compiler
| error. If you really must do unsafe things, you must
| surround that code in an unsafe block, which serves as
| documentation that must be kept up to date, or the code
| will not compile. Others can inspect your code and very
| easily decide if they want to use it based on how much
| unsafe code they are willing to accept.
|
| I don't want a compiler that will only ensure certain
| kinds of correctness if I already know how to write
| things in the "correct way" (which has traditionally been
| a moving target in the C++ world). I want a compiler that
| will ensure those types of correctness always, and reject
| programs where it can't give me that guarantee. The C++
| compiler will not do that, but the Rust compiler will.
|
| > _In modern C++ you get compile-time correctness by
| construction_
|
| No you don't! You only get this if you've written your
| code correctly! The entire point of "correct by
| construction, verified by the compiler" is that you
| cannot write the code in an incorrect way that the
| compiler will accept. And the C++ compiler absolutely
| will accept "old-school C++" that could be incorrect. Put
| another way, there is no C++ compiler that will only
| accept "modern C++" and reject "old-school C++". And even
| if someone were to try to write such a thing (I am
| skeptical it's possible), reasonable, knowledgeable C++
| developers could easily disagree on what should and
| shouldn't be included.
| skohan wrote:
| Just to give an alternate viewpoint, I'm currently writing
| most of my code in Rust, and while I am generally a fan of
| the language, and a _huge_ fan of the tooling and community,
| I think Rust often gets a pass on complexity because it 's
| mostly compared against C++ which is itself a monstrosity.
| There are many, many other languages to which Rust seems
| insanely complex and difficult to program in by comparison. I
| think that often gets blamed on accommodating the borrow
| checker, and Rust's low-level nature, but I think a lot of it
| is avoidable.
| steveklabnik wrote:
| I'm curious, not saying you're right or wrong: what bits do
| you think are avoidable?
|
| Personally, I agree that Rust is complex, but almost all of
| that complexity is inherent to the kinds of tradeoffs Rust
| makes. With different tradeoffs, I can certainly imagine a
| much simpler language, but it's less clear to me what stuff
| could be significantly simplified without doing so.
| However, I am extremely biased!
| skohan wrote:
| My go-to example would be the module system:
|
| 1. 90% of the time, rust module layouts are basically a
| copy of the file-system heirarchy, so why do I have to
| type this? This system would be much more approachable if
| it gave the file-system hierarchy as the default module
| layout, and let you override it explicitly. The argument
| I have heard against this is that some people like to
| comment out module declarations during debugging. I don't
| find this compelling, because I don't see why you
| couldn't have an explicit way to ignore a module instead.
| This is just one of many cases where Rust seems to
| prioritize the edge case at the expense of the common
| case.
|
| 2. On top of requiring a lot of boilerplate the module
| system is quite esoteric. I've had to learn it twice: a
| few years ago I was dabbling in Rust, and I remember
| having to struggle a bit to understand how to add a
| second file to my project, and then about a year ago when
| I was getting back into rust I found it unintuitive a
| second time. I challenge you to find someone who is
| unfamiliar with the module system, and see how long it
| takes them, using _only_ the documentation, to figure out
| where they have to put the `mod` declarations to add
| nested submodules to their project to make it compile.
| Maybe I am unreasonably thick, but I don 't think it's my
| problem since I've worked with a lot of languages, and
| never had this much trouble.
|
| 3. This esoteric system isn't even deterministic. Imagine
| I have the line `mod foo` in my `lib.rs`. Where can I go
| to find the source for that? Well, it depends. It could
| be in `src/foo.rs`, or it could be in `src/foo/mod.rs`.
| And let's say I'm using external crate: `use some_crate`.
| Which import does that correspond to? It could be:
| `some_crate`, or it could be `some-crate` in my
| `Cargo.toml`. You just have to kind of know all of these
| implicit behaviors of the compiler to know what's going
| on.
|
| So this is just one feature, but IMO it's just one
| example of a case where Rust puts very little emphasis on
| the UX and understandability of the language.
| iudqnolq wrote:
| I definitely think you're right that the module system is
| complicated, even the new module system. I would like to
| make one nit though: it's definitely deterministic.
| Deterministic just means it can be predicted, not that
| you specifically can predict it. If the module system was
| non-deterministic that would be a much bigger issue
| skohan wrote:
| Ok fair enough, that is true. I guess it would be more
| accurate to say that a module declaration is ambiguous
| with respect to the file-system location of the code it's
| referring to.
| steveklabnik wrote:
| Cool thanks! This is a great example of my mental bias,
| actually: I don't tend to think of the module system as a
| "language feature" even though it clearly is!
|
| There are some good reasons and interesting arguments
| around all this, but given that I was just curious about
| your opinion, I won't bore you with all that :) Thanks!
| skohan wrote:
| No worries, happy to share!
| skoodge wrote:
| I'll chime in and say that for me at least, the module
| system was a hurdle at the start and coincidentally the
| only part of the language where the explanation in The
| Book (which is excellent, so thank you, btw!) did not
| click for me.
|
| I think it's debatable whether the module system really
| is complex or just different from what newcomers are used
| to (and by now I've grown pretty accustomed to it). But
| in contrast to most other language features, where it was
| clear what I was getting in return for the steep learning
| curve, the module system seemed overly complicated at the
| time for no real benefit. Not a big issue by any means
| and I would choose Rust with its module system over the
| alternatives most days of the week, but it is one
| tradeoff that to me at least seemed orthogonal to the
| other borrowing-related complexities.
|
| (What is more worrying to me nowadays is the whole async
| story. I do hope that some of it will get better once
| certain features land and it is certainly an area where
| some additional complexity is unavoidable, but it is the
| only part of Rust that I dread touching despite heavily
| using async in a moderately sized personal project due to
| the need for WASM + IndexedDB, simply because lifetime
| issues become much more tricky once async and either
| traits, recursion or closures are involved. By now I am
| consciously trying to limit any async parts of the
| program to a simple and stupid "Rust-light" style without
| any "fancy" features such as traits or closures, which
| does not feel like a proper solution. So yeah, in general
| I agree with you: Rust is certainly complex, but for the
| most part not unnecessarily so.)
| steveklabnik wrote:
| Thanks!
|
| Yeah, teaching the module system is kind of my white
| whale. Carol and I have spent more time on that part of
| the book than almost any other; re-written like five
| times.
|
| My current working theory is that most people assume that
| "the module system" is similar to whatever one they've
| used in the past, then run into problems, and leads to
| frustration. I've talked to so many people who have
| totally opposite problems with it, with no real pattern
| to issues or expectations.
|
| I think that it's very straightforward, personally, with
| very very simple rules (especially in 2018). But I
| certainly acknowledge that I am the exception, not the
| rule.
| orthoxerox wrote:
| > My current working theory is that most people assume
| that "the module system" is similar to whatever one
| they've used in the past, then run into problems, and
| leads to frustration. I've talked to so many people who
| have totally opposite problems with it, with no real
| pattern to issues or expectations.
|
| When I treated modules as Java packages or C# namespaces
| I hated them. Only when I realized that I can treat them
| as glorified C imports did they start to make sense. I
| still hate them, but at least I can rationalize their
| design now.
| rabidferret wrote:
| > I think that it's very straightforward, personally,
| with very very simple rules (especially in 2018).
|
| I agree with you. It's a shame we don't have a simple
| visual metaphor to describe this
|
| EDIT: Wait does hacker news just delete non-ascii? I
| tried to put an upside down smiley here how is this even
| worse than I thought
| steveklabnik wrote:
| https://news.ycombinator.com/formatdoc is the only actual
| description, but it does seem that non-ascii text is
| ignored, yes.
| saagarjha wrote:
| The rules are mainly there to filter out emoji; it can do
| most non-ASCII outside of that.
| L[?][?][?]i[?][?][?]k[?][?][?]e[?][?][?]
| Z[?][?][?]a[?][?][?]lg[?]o[?][?].[?][?]
| estebank wrote:
| You might be interested in taking a look at and
| potentially participating in the "Async Vision
| Document"[1] which is an exercise the team is going
| through to collect feedback about the current state of
| the ecosystem and what the pain points are, as well as a
| way to lay doing what the desired future state of async
| Rust should be[2]. The process is happening, as you would
| expect, in the open and there's still time to influence
| it[3] if your concerns aren't yet addressed or even
| mentioned[4].
|
| [1]: https://rust-lang.github.io/wg-async-
| foundations/vision.html
|
| [2]: https://blog.rust-lang.org/2021/03/18/async-vision-
| doc.html
|
| [3]: https://github.com/rust-lang/wg-async-
| foundations/pulls
|
| [4]: https://github.com/rust-lang/wg-async-
| foundations/issues
| bsder wrote:
| > By now I am consciously trying to limit any async parts
| of the program to a simple and stupid "Rust-light" style
| without any "fancy" features such as traits or closures,
| which does not feel like a proper solution.
|
| I disagree that this is bad, and I think you've made a
| _good_ decision.
|
| I find that too many Rust programmers reach for a closure
| _WAY_ too often and, even when they should grab a
| closure, they make it far too complicated. Closures
| should be _short_. Inline closures are nice when they 're
| a single line part of "collect()" (although, I've seen
| some that make me want to hang the author ...).
|
| However, if your closure is 15 lines long invoking a
| builder chain (this is a common issue in EventLoop type
| closures), that should be a _function_. This is before I
| get started about how builder chains are a gigantic
| misfeature to paper over the fact that the language doesn
| 't have default/named function arguments.
|
| Anyway ... I have found that "make it a named function
| and call it" is often a far better way to communicate
| exactly what your intent was.
| skohan wrote:
| > builder chains are a gigantic misfeature to paper over
| the fact that the language doesn't have default/named
| function arguments.
|
| Totally agree. I think it's telling that Rust Analyzer
| basically inserts argument labels inline in the editor,
| and this is the preferred way to work with Rust.
| doggodaddo78 wrote:
| Good god. Wicked, bad, naughty Rust. Oh, it is a naughty
| language, and it must pay the penalty -- and here in Castle
| Congruence, we have but one punishment for setting alight the
| grail-shaped semantics.
| ivan888 wrote:
| Languages like this: fun to author the grammar, and have the
| entire thing in your head. So expressive! Difficult for anyone
| else (including yourself in 2 years) to understand
| steveklabnik wrote:
| Virtually any language can be "like this," these examples are
| all nonsense designed to stress the compiler. This code isn't
| supposed to be understood by any human.
| augustk wrote:
| "Another lesson we should have learned from the recent past is
| that the development of richer or more powerful programming
| languages was a mistake in the sense that these baroque
| monstrosities, these conglomerations of idiosyncrasies, are
| really unmanageable, both mechanically and mentally. I see a
| great future for very systematic and very modest programming
| languages."
|
| -- "The Humble Programmer"
|
| https://www.cs.utexas.edu/~EWD/transcriptions/EWD03xx/EWD340...
| kubb wrote:
| I mean the fact that you can do funny stuff with the syntax
| rules and the type system doesn't disqualify their usefulness.
| Rust's type system is impressive as it enables enforcing
| powerful constraints over how the code is allowed to execute,
| eliminating whole classes of bugs, and enabling elegant, zero-
| cost abstraction over swathes of algorithms and data
| structures, which in turn lets you have high quality libraries
| that provide to-the-point solutions for so many of the common
| tasks that a developer will deal with day to day.
|
| A more elegant language that solves the problems that Rust
| solves, with a production-ready implementation and a good
| ecosystem hasn't been presented to the world yet. It's to be
| expected that if you want to benefit from its advantages, you
| need to pay the cost of dealing with a more complex language.
| On the other hand paying the cost of being exposed to more
| concepts pays off by giving you more flexibility to craft
| simple solutions. Complexity of a project can vastly exceed
| that of the language the project is written it. In that case
| the cognitive cost of using more advanced tools to manage that
| complexity is a boon in the long run.
|
| This is analogous to any kind of more sophisticated technology,
| e.g. you can argue that a scythe is easier to use than a
| harvester, but with a big enough field, if you have the option,
| you'll go for the latter.
| pcwalton wrote:
| Is it complexity, or is it the result of the _simplification_
| of unifying expressions and statements in most cases?
| shakow wrote:
| The problem is that the complexity in computing is
| intrinsically irreducible. Sure, you can get nice nifty little
| languages, but then the latent complexity will be transferred
| into the program itself (e.g. program length, cyclomatic
| complexity, etc.) rather than in the language; that's why
| although they can technically do the same thing, we have a
| spectrum of languages ranging from assembly to APL.
|
| Practically, it's exactly what comes out of the infamous Rust
| vs. Go debate: for instance, the former prefers a short and
| concise albeit arguably more complex encoding and processing of
| the errors based on a monadic type, whereas the other leans
| toward a simpler, but more verbose, double return value +
| if/else/early return. Chose your poison.
| fanf2 wrote:
| Dijkstra is best read as food for thought, since he is more
| often interesting than right.
___________________________________________________________________
(page generated 2021-05-14 23:00 UTC)