Better line numbers is huge. Actually huge for people leaning rust. In a pretty big way it's like the difference between Java where you get a stack trace (where you mostly care about the live number all the way at the bottom) and C++ where you don't. When I tutored students in college that was one of the most salient differences to students who were just starting out. They would say things like "C++ is hard because you don't even get a stack trace". Sure, that's a naive view of the differences between the languages, but it's a real view that beginners see.
C++ does give you a stack trace; you just need to set some things up before it does (namely, use a debugger). Rust does the same except it tells you what you need to do (set an environment variable).
You get this single-line error without enabling backtraces however. So the fact that this is now useful for the common case of "which line caused the error" is a pretty big deal.
Yeah for backtraces to work you also need to have debug info enabled which puts a major burden onto linking time. And once they are enabled, you have to scan the backtrace carefully to filter out the parts that are in the panic library. The line message is displayed prominently and thus allows for a much faster development cycle in finding the error.
I mean, they're better than nothing. But they leave you guessing at exactly where the error is if you do similar operations more than once in your function. A line number is much better.
Just as a life tip (regardless of language): Without a line number you would get an offset, which you can use gdb or addr2line to go back to line numbers if you have the unstripped binary or debug symbols available (or the source, but that’s not necessarily deterministic).
There's backtrace_symbols, but to get decent info you need linker flag -rdynamic. I much prefer the stack traces from Rust, or Java (Apples to Oranges comparison there, as comparing a compiled language trace to one from interpreted language). For new projects needing a systems language in permissive environments I see no reason to go with anything other than Rust or Go, though there are other options that might be as good that I'm not familiar with. Unfortunately, there's a lot of legacy code (there are still people getting paid to maintain COBOL programs - not me though), and there are a number of restrictive environments where any technology newer than 8 years is a tough a sell.
I agree, I’m glad Rust is addressing it, one the most annoying things about D is the lack of stack traces and is the reason I stopped using it and switched to Go entirely, I wrote a blog post[0] about D vs Go and their approaches to stack traces, at least Rust acknowledges the problem.
C/C++ programs will normally have function names in backtraces, just not line numbers.
$ cat test.c
int g() {
int *p = 0;
*p = 5;
}
int f() {
g();
}
int main() {
f();
}
$ cc test.c
$ ./a.out
[1] 3377 segmentation fault (core dumped) ./a.out
$ gdb -q a.out core
Reading symbols from a.out...
(No debugging symbols found in a.out)
[New LWP 3377]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000557379b7613d in g ()
(gdb) bt
#0 0x0000557379b7613d in g ()
#1 0x0000557379b76158 in f ()
#2 0x0000557379b7616d in main ()
Another wrinkle is that the symbol names of extern functions need to be in the binary regardless of debug level, though the main binary is a special case, unlike shared libraries. So you'll get different results again if you add -fPIC -rdynamic (or possibly just -fPIC) to -O3, similar results as-if you compiled a library with -O3 -fPIC -shared. But then if you define f and g as statically scoped -fPIC -rdynamic won't change the behavior.
It follows that there's a potential conflict between code optimization and the ability to determine function names and line numbers for a trace. To preserve the ability to associate the programer counter (PC) to distinct function names and line numbers a compiler might need to abstain from completely inlining, merging, eliding, or otherwise optimizing some functions and code blocks (though that doesn't mean it needs to preserve actual function call behavior). People will endlessly debate the cost+benefit, but it's something to keep in mind for performance critical code blocks.
> As they are specifically talking about new programmers, using gdb in your example effectively communicates and confirms their point
New programmers would be using IDEs, press the green arrow "play" button and launch the app with the debugger enabled without them knowing, which would cause the editor to jump directly to where the issue is
I'm sorry but there are bad teachers / courses everywhere, that does not reflect on the technology (though C++ has it really bad for some reason). When I was in school learning how to debug was one of the first classes we had.
If they were teaching you rust be sure that they wouldn't have told you about cargo either.
That is a reflection of your school, and not the language.
I'm a self taught C/C++ programmer, learning and becoming comfortable with gdb was one of the first few things I taught myself after learning how to compile code into a binary back in the 1990's.
That's a naive view. Depends who's learning. Is C++ their 5th language and they know how to read resulting assembly? Is it their first experience at 10yo and they're struggling with what variables and types are? Anywhere in between?
Gdb beyond what the learning person can comfortably handle at the time and that's what matters.
Being able to use a debugger should be an essential part of any curriculum. GDB is only an advanced tool if you're an advanced user; it's useful for the entire spectrum of programming language users.
That everything is relative. That gdb absolutely is advanced and complicated dark magic if you’ve learned to code/debug on and been spoiled by an easy to use IDE.
C/C++ compile some weird default of neither debug nor optimized, basically the least helpful thing anyone could want (no performance optimizations, no debug symbols, and maybe-maybe-not asserts depending on if the standard library you’re using or the libraries you’re depending on used #if NDEBUG or #if DEBUG to gate the assertion checks. And not optimized for size either, if that’s what you were wondering).
Whether a build is debug or release is independent of whether it contains debug symbols. Both `cargo build` and `cargo build --release` produce builds with debug symbols.
I think you still get symbols for libstd without that. I can see why that would lead to confusion - stripping a plain --release build still saves a few MB.
To get the callstack before the exceptions unroll, on Windows you can utilize https://docs.microsoft.com/en-us/windows/win32/api/errhandli... but then you would have to skip few specific exceptions (that are normally issued, since these come really fast, and you'll see them)
Unless I've misunderstood the context, you can just open the "exceptions" debugging pane and check a few checkboxes. Then when the exception gets thrown the whole thing stops in a debugger and you can do all the usual things including look at the stack trace, indirect local variables at the throw point, etc.
Sorry, I'm lately in the "CI" mood, testing, automation so bit became a bit of habit to spawn what I've learned last few months.
The Visual Studio Debugger is simply the best. Okay, it's not a powerhouse like WinDBG, or have the knowledge of IDAPro, and various other cool debuggers (OllyDbg, others..), but it's the easiest one to learn (IMHO), and teach. Few bits and tricks, press F5 and you are done :)
Ah OK. But the context was "students in college ... who were just starting out". I would say they would be running their programs, at least initially, in an IDE. And they should be using an IDE to develop: getting students to develop without IDE support is just unfair (although I know people get religious about using a text editor for code development). This is how I keep finding new grads who have no idea how to use a debugger e.g. just set a breakpoint and inspect variables.
Is this still a thing? I've never encountered Dr.Watson recently. I do remember really long time ago Soft-ICE - that was exciting back then - debugging DOS/Windows really low level (I think it was through serial port on another machine)....
In Windows, you’ll typically always have symbols available regardless of the compilation mode since PE executables (always?) store their debug info and symbols in an external debug symbols database file (.pdb) rather than in the binary itself. (This is now possible with ELF/DWARF too, I believe).
> Due to our stability policy, description will never be removed, and so this is as far as we can go.
What Rust _should_ do is gradually increase the noisiness of warnings about using this. Eventually it'll disappear from the crate and end-user ecosystem (possibly with a small effort to tidy up abandoned crates).
Java's "never remove a deprecated API" was definitely one of their big mistakes. Even a 10-year deprecation cycle is better than none.
Java actually removes deprecated APIs now, and it's doing it really really quickly (IMHO way too fast).
For example javax.activation, javax.activity, javax.rmi, javax.transaction, javax.xml.bind, javax.xml.ws, javax.jws, and javax.xml were deprecated in Java 9 (released September 2017) and removed in Java 11 (released September 2018). JavaFX was deprecated in Java 10 and removed 6 months later in Java 11.
Dealing with Java 11 was really painful to the point where it really feels like Python 3 of Java. The company I work for had less issues with migrating to Python 3 than with migrating to Java 11, but your mileage may vary.
Ditto with the the other Java EE API’s GP mentioned, their interfaces are available on Maven Central instead of being bundled in the JDK - which makes sense as Java EE is not actually a thing anymore and the Eclipse Foundation took over as Shepard.
In an effort to shave a couple minutes off of a build process, back when everyone was still drunk on XML, I got into a conversation with one of the webmasters at W3C. A lot of people forget to cache public schemas and DTDs because the libraries don't by default and the setting is a bit buried.
We were trying to convince him to sandbag responses a bit. I never followed up but it sounded like we almost had him convinced. My thinking was, I didn't notice the problem until we hit almost 2 minutes. If their website hadn't been so fast I probably would have identified the problem months earlier. Essentially his team were being punished for being so damned good at their jobs.
I wonder how palatable it would be to slowly ramp up a pause in the compiler for "No, really, you need to stop doing this, and soon" types of misfeatures. Not a huge one, just enough so people really notice.
> Java's "never remove a deprecated API" was definitely one of their big mistakes. Even a 10-year deprecation cycle is better than none.
Can you explain this position? Python just did a 10-year deprecation cycle on Python 2, but it was really costly for a lot of people. If they'd just not done that it would have been better; the benefits of removing stuff are relatively minor (cleaner language implementation codebase) and the costs are huge (all users need to change, test, handle compatibility, ...).
No, no, no. It was not a deprecation cycle. It was a complete stop using this because it's full of unclear semantics, here's the new thing cycle.
Py3 should have simply started to warn when you used "bla" without u"..." or b"...", and so on. To clear up the bad code. Then later ... maybe 10 years later, make "..." equivalent to u"...". Maybe.
I don’t know what the right solution to the string problem should have been. Python 2’a string handling was bad, and programs used it inconsistently and nonsensically.
IMO the real problem is the near complete lack of static checking that you fixed it with. Sure, modern Python + mypy can do a credible job finding wrong-string-type bugs, but legacy Python 2 codebases don’t have the right annotations.
I do wish I could tell Python 3 to error out if I accidentally put a str and a bytes in the same dictionary as keys.
The core problem with strings in Python 2 was implicit coercion: you could mix `b''` and `u''` strings pretty freely as long as they only contained ASCII and they would be silently converted as required. Once you leave the ASCII range you start to see data-dependent bugs.
To gracefully deprecate this behavior, you could start by generating a warning each time an implicit coercion is done. Next, make implicit coercion raise an exception, but provide a way to suppress it. Finally, remove the ability to suppress the exception.
As the GP suggests, you'd do well to similarly deprecate unprefixed strings.
This would all be pretty confusing to explain if you were renaming the string types at the same time, as Python 3 did. I think that's an indication that you shouldn't rename the types. You could deprecate `str` and just use the names `bytes` and `unicode`, which go nicely with the `b''` and `u''` mnemonics anyway.
Python 3 also changed the type of string used for Python identifiers. You'd need a strategy there as well.
It might be convenient to have some type-checking `dict` variants in the stdlib, but I think it's a separate issue from addressing the coercion issue.
IMHO, it would have been to repurpose the existing format for Unicode (specifically UTF-8) text and have a special notation for binary/ascii text.
It’s interpreted code, ffs. It’s not like you have pre-compiled binaries you need to stay ABI compatible with (like Microsoft did when they introduced TCHAR and co).
Among other problems, there was never a time when you could combine python 2 and 3 in the same program. That wasn't a deprecation; it was simply a break.
Rust has Editions, so deprecated features can slowly age out of the ecosystem. People using, say, the 2015 edition will probably eventually want features introduced in the 2018 edition, and they will have to remove their usage of features that were dropped in the 2015->2018 edition bump. And if they never want any new features, maybe because their library is totally complete? Well, crates using 2018... can still depend on them, they just can't use those features.
And other features that differentiate them from C++. But I think that was the OP's point: People are switching to other languages mainly because C++ has become a monster, not because of missing memory safety.
I don't think that's the whole story. Golang is mostly picking up programmers from dynamic languages, according to Rob Pike, and Swift is gaining steam mostly because Objective-C while not monstrously complex like C++ is incredibly verbose.
Swift is gaining steam, because Apple dictates it, unless one enjoys using a frozen language.
Likewese using C++ on Android requires perseverance given the how the NDK is handled and what it exposes, while on Windows it feels nice because it has parity with .NET tooling, specially on UWP.
Using a frozen language isn’t the real problem here, though.
The Apple ecosystem slowly gains new frameworks like SwiftUI that simply don’t work with Objective C anymore. The platform is abandoning one language similar to Microsoft and their VB story. It will take a long time, but AppKit & UIKit will end similarly to how Carbon ended on macOS.
Swift can't remove things from the standard library anymore either. Otherwise if the OS updated, and a symbol was missing, apps needing that symbol wouldn't launch.
What they should do is put as much of `std` on crates.io as possible (only a tiny bit is rustc-specific). Then in conjunction with https://github.com/rust-lang/wg-cargo-std-aware uses can opt-in to to a `std-2.x`.
The function itself cannot be removed from the standard library (and likewise the symbol can never be reclaimed for a different use), but there's no reason that future editions couldn't report a compiler error when attempting to use the function. The function would only be inaccessible at the source level, not at the linking level, thereby completely preserving cross-edition compatibility while also mandating that crates remove references to it when migrating to a newer edition. Think of it like an edition-specific deprecation lint being turned from warn to deny.
Idk how well-known this is, but Java 4 was actually Java 1.4. At Java 5, they dropped the "one point" marketing, but still called it 1.5 to devs. I'm not sure how this was resolved, or if there will ever be a Java 2.x (I doubt there will).
I'm not so sure. Ruby did a very similar set of breaking changes around the same time and it went fine. It seems to me like the main difference wasn't in the changes — it was that the Rails developers enthusiastically embraced the changes, which pushed everyone else to do the same, while Python had more of a diffuse situation where some library authors went for it and others didn't and it led to everybody getting cold feet.
Like I said, I have trouble buying that as the reason, because a very similar Ruby release right around the same time did not cause the same problem. The major change in both cases was that strings now force you to consider encoding rather than just treating everything as a blob of probably-ascii-but-whatever-who-cares bytes. Writing code that was both Ruby 1.8 and 1.9-compatible wasn't easy, and there wasn't something like six to help you do it. The Ruby community generally responded by just supporting Ruby 1.9 going forward, and there was no major schism.
I think Ruby was helped by its great tooling (gem, lockfile, bundler), plus it was just advertised/communicated/presented as the next logical step forward, not a major release, no talk about supporting the old release for years, etc. (I don't know about Ruby's LTS policy, but the point is that 1.9 was not special, so it did not instantly made 1.8 "sacred" too.)
> I disagree... you could always leverage six and/or do conditional imports based on sys.version
Sure it was possible, but what benefit would doing all that work get you? More memory use! It wasn't until Python 3.5 that Python 3 was clearly better for most use cases. Even today the regression import performance is problematic for CLI tools.
The point is that the whole py2-3 change was not incremental enough. (We started an ecommerce project right around 3.4, and many, many libs were in shambles, no official py3 support, just random forks on GitHub. The whole Python ecosystem was in a rather sad state. No mypy, no pipenv/poetry/dephell.)
All the gashing of teeth spent on the useless flamewars about py2/py3 could have been spent on other things.
> Usually after searching for answers for quite a while on Stack Overflow...
That's the problem. Py3 was basically a hard fork with no real benefits for years, but by then it became a sort of ideological rift, and people started back-porting stuff instead of upgrading.
I didn't realize how rich the normal match { } patterns can get until I saw these matches!() examples and was trying to figure out what that pattern syntax was. I've only ever used the basic enum matching/enum value unwrapping patterns.
A similar macro the stdlib should have is poor_mans_cast!(foo as Bar::Qux) which expands to match foo { Bar::Qux(x) => Some(x), _ => None }.unwrap() which is a questionably-hacky way to cast to a specific enum variant. I've had to use it twice because I couldn't figure out a more Rust-like way to solve the problem at hand.
Think for example of implementing a list interpreter. In certain situations, you know for a fact that an AST node is a given type. So you just want to cast from Node to Node::Symbol(s) to extract that std::string::String out of it. RFC #2593 seems like it would help here, and is something I've wanted for similar but unrelated reasons. So in the meantime I made poor_mans_cast!() macro to do this, which I've used twice already.
It looks like they want to panic if the variant is not what they expect. Basically implement their own unwrap() for their own enum, since the above logic is basically what Option::unwrap() does for Option::Some.
Assuming there's not some wider design issue, I'd probably implement an unwrap_qux() method on the Bar enum
I'm actually planning on declaring war against pattern matching in EcmaScript, which is stage 1 right now. I can't believe how popular it can be. To me it looks like a huge antipattern, a complete different way of writing conditionals from ifs, built in a way that can lead to completely unreadable code (for complex matching) and very inconsistent behaviour. And just like switch-case, become something to avoid outside of byte-sized or plain literal matching.
Not to mention how much each language differs in how the syntax and behavior is implemented, making it harder for beginners everywhere to ramp up.
Like the smart-match clusterf* in Perl, pattern matching is just too darn clever for its own good.
Rust’s pattern matching is deliberately fairly limited in what it can do; it’s all about destructuring types, not running arbitrary code like Perl’s smart-match (which even so I would not describe as a disaster). It’s conceptually very clean—arguably simpler than what ECMAScript already has. `let PATTERN = EXPRESSION;`, `if let PATTERN { … }`, `match EXPRESSION { PATTERN => … }`, `fn NAME(PATTERN: TYPE)`, &c., everything that can introduce a new binding is a pattern. Some of the uses of patterns are refutable (match branches, `if let`, `while let`), and some irrefutable (`let`, `for`, function arguments, closure arguments).
ECMAScript already has destructuring in various places, corresponding to irrefutable pattern matching; what the proposal’s `case` expression introduces is essentially just refutable destructuring, plus a smidgeon of expression-orientation (`->` corresponding to `=>`, that `case` is an expression that yields a value, rather than a statement) in a place that sorely needs it. This is a very logical extension of the language.
If TC39 or equivalent were to design a new language to actively replace JavaScript (meaning something that all extant JavaScript code should be able to migrate to easily, preferably automatedly), there is no doubt in my mind that the language would be more expression-oriented than ECMAScript is, that destructuring syntax would be brought in line with what we call pattern matching so that there was one coherent concept underneath (probably called pattern matching), and that `switch` would use it, becoming equivalent to this proposal.
The way people use JavaScript these days, these things are useful.
(I write all this as an expert and heavy user of both Rust and JavaScript; I use JavaScript more, most of the time, but prefer Rust. Rust was the first language I learned with each of algebraic data types, pattern matching and expression orientation.)
Perl smart match IS a disaster, we fought with lot of bad smart-match behavior for a while in the late 2000s, then educated programmers over and over on how to do smart matching just to see how the Perl community and the language took action to deprecate most of it. We then dove into 100k+ lines of code to remove all smart matching entirely, including all `when` clauses. To me that's the definition of a disaster for a programming language.
Pattern matching is a can of worms in the way people think these should work and the way compilers implement them. It holds unexpected behavior in so many ways and it's not at all about language abuse, but about giving a tool that is prone to misunderstanding and flimsy enough for programmers to shoot themselves and others in the foot. And it's not the same pattern matching in Haskell (which is fine) that it is in ES (where it's not).
Fortunately the TC39 proposal looks somewhat stalled and you can already see in the Babel plugin and proposal discussion questions on why or how this does or does not do behavior X, Y or Z or how syntax should be implemented. This is a bad sign on how pattern matching can be confusing, but then maybe this may be a sign it never gets past stage 1. Here are a few soundbites from the proposal:
when true -> ... // ok, if x === true
when undefined -> ... // not ok, this creates a local var called "undefined"
when Infinity -> ... // ok, if x === Infinity
when -Infinity -> ... // Syntax error!
when /.*/ -> ... // not ok, unsupported, surprise surprise.
when {x} = {x:1} -> ... // great, we check if x exists and set a value on it if it doesn't?!?!
when {status = 200} if (status === 200) -> ... // left as an exercise for the reader
Now I just love this one, as it just subsumes many of my concerns:
const y = 2;
case (1) { // matching on a value itself, lovely
when y if (y === 2) -> 'does not match', // y is (re)created locally for the if!
when x if (x === 1) -> 'x is 1' // you're a psycho if you write code like this
}
Now, I must confess I do like matching on types as they can substitute method dispatching elegantly and should be simple to understand and implement, but this is more suited for typed languages, not ES. Same for basic parameter existence as a destructuring dispatch for arrays and objects. But never on value, ranges (`[1..=x]` - yuck, Rust!) or anything more complex.
Perl smart match was a disaster because it was symmetric.
Historically, smart match in Perl was "stolen" from the ideas of Perl 6. Perl 6 (now named Raku https://raku.org using the #rakulang tag on social media) learned from the problems found in Perl, and changed its smart-matching model from symmetric to asymmetric. Basically, `a ~~ b` is syntactic sugar for `b.ACCEPTS(a)`. By transferring the responsibility for what smart matching means to an object of a given class, made it possible to make much more sane default behaviour, as well as allowing authors to define their own smart-matching behaviour for custom classes.
Any language feature can be misused. In my opinion JS doesn’t need it, because destructuring already exists for the case where you just want to be more expressive about extracting values. And it makes far less sense than in Rust where the type system enforces exhaustive matching.
Part of the problem is that Perl doesn't have as strong of a type system.
(Is is an int or a string, or is is both? The runtime certainly doesn't know.)
It was also symmetric in 5.10 (changed in 5.10.1), which didn't help.
---
The major mistake was not making it experimental from the start.
(The smartmatch feature is why marking things as experimental is now a thing.)
---
If it is carefully designed, it should prove to be useful just like it has in Raku.
If it is carefully designed, it should prove to be useful just like it has in Raku.
I don't see how it can be designed well enough. The fundamental problem is that Perl operators are monomorphic and variables are polymorphic. This was the same problem for things like keys and each on references, for example.
Would you please explain why you think pattern matching is an anti-feature? I used it in Erlang and liked it. I think pattern matching can make code really clear and concise. I missed it when coding profesionally in Java, C++, and Golang. But I never used pattern matching in a large codebase. Maybe I missed its negatives?
> Due to our stability policy, `description` will never be removed ..
Can this sort of removal not happen as part of an edition? Is it because it's part of std, rather than core, or something like that? It's a bummer, but the commitment to stability is appreciated!
matches!, subslice patterns, and .unwrap messages look excellent!
Editions do not permit arbitrary breaking changes. Notably, the same version of std must work across all extant editions of std. For example, many people use the `regex` crate in their Rust 2018 projects despite the fact that `regex` still uses Rust 2015. If Rust 2018 made a breaking change to std, this wouldn't be possible in general.
You could add an edition sensitive non-overridable error to the language, as in, if you use it on edition 2015 or 2018, there is no problem, but on edition 2021 and beyond there will be an error in your code that you can't allow away. Then you could have edition specific rustdocs for std where items that are deprecated this way are hidden per default.
However, at least the 2018 edition had the requirement that updating can be done automatically. Ensuring that is hard for library functions.
The problem is that it would be possible for a Rust 2015 module to encounter an Error defined in Rust 202X, so what would happen if that Rust 2015 called `description` on that error if Rust 202X didn't require/allow defining the method `description`?
If your main crate is edition 202X, then it could warn if you link it with anything that depends on Error.description. Too drastic (and would be dog slow)? Yes, but it's easily possible.
Bad person to ask. I am personally terribly unproductive with Python. I might be able to hammer out a 50 line script that does some data shuffling faster than I could write an equivalent Rust program, but anything more than that---especially when it comes to maintaining it---and Rust easily wins for me in terms of productivity.
Nothing against Python specifically. I've just tried and failed over and over again to build large programs in untyped languages. I like my types and think they are invaluable, especially when it comes to refactoring.
I've worked professionally using something like 20 different programming languages. Heck, I use 4 or so in my current job. Productivity as a programmer is rarely influenced much by programming language, as long as the programmer is proficient in that language, IMHO. I've never actually used Python professionally, but I've programmed for years in Ruby and Javascript as well as C#, C++, etc, etc. The vast majority of complexity in a code base is added by the programmer themselves and that complexity has less to do with programming language issues than it does with design (or lack thereof). Even on small projects, the ability to do something quickly is almost completely due to having a reuse library that gets you most of the way there coupled with the knowledge of how to use that reuse library. Experts in particular ecosystems can often bang out code at frightening speed. Similarly, complete horrible messes can easily be written in any programming language.
Rust helps you deal with certain classes of problems. It forces you to write code in a certain way and think in a certain way. Especially if you are using the standard library, you have less flexibility on how you can write your code if you don't want to write a complete mess. In many ways this is an advantage -- especially if you are very experienced with the ecosystem. Programming languages like Python or Ruby and especially JS (which has a terrible standard library) offer more flexibility. However, it comes at the cost that it's easy to get yourself into trouble.
In the end, these things usually come out in the wash. Programming is programming. Some people are always going to like doing things one way or another. I actually love writing code in JS because I find it a very expressive language (and my code looks nothing like most people would expect JS code to look like). In some ways, Rust is like programming in a straight jacket, but it's a comfortable straight jacket :-). I like the shape of code that Rust encourages me to write. As I get more proficient in the language (not so proficient yet), I find that I think less and less about how to do things and more and more about what I want to do -- like any language, really.
The main downside of Rust is that it is huge and opinionated. It takes a long time to learn how to use it well and it really wants to push you towards certain types of solutions. Learning all of these things is challenging.
TL;DR: Yes, you can definitely be as productive in Rust as in Python. And vice versa. Modulus the fact that you will be running into different problems with each.
I guess that it _would_ be possible to have a hard "edition lint" that would basically forbid you from _implementing_ description in a 2021 or whatever crate. It would still exist and be callable, but no new (non-default) implementations if you want to use new editions.
Very grateful about the better unwrap location reporting. This has annoyed me last this week. Didn't know that it was being stabilized (only checked the track_caller tracking issue which is still open) so I'm very happy now :).
A good idea usually is to use unwrap only if you know there is no logical way for it to blow up and to use .expect("My Message explaining why this blew up") everywhere else.
"Added tier 2 support for riscv64gc-unknown-linux-gnu"
which hopefully means Rust will make its way to the RISC-V versions of Fedora/Debian (wasn't there when I last checked). Furthermore, there are a lot of things depending on this that might also arrive now.
I'm curious to know if anyone who regularly uses Rust could speak to a theory I have on the language. I'm by no means an expert but I've dabbled enough to know Rust is I think going to be far more important to the world than it is already.
I'm planning to dive back into it, but this time I'm going to attempt to do all of my programming in such a way so as to not require 'lifetime' semantics.
Not 100% sure how easy it would be to do so, but the theory is that maybe writing code that needs lifetime semantics is an anti-pattern? Rust has just given us the tooling so we can confirm that the "style" we are already writing our code in adhere's to Rust's guarantees...
Using Rust daily since ~2016 here. I'm assuming that by "writing code that needs lifetime semantics" you mean "writing code that needs explicit lifetimes", since all code in Rust has some lifetime semantics going on. If I've misunderstood then please clarify.
Lifetimes are one tool in the toolbox, and using them is absolutely not an anti-pattern. Being able to define and use explicit lifetimes is a really powerful tool that allows you to safely share data in ways that can be tricky and error-prone to do in some other languages.
For example, a deserialiser could be passed a reference to a buffer (&'a [u8]) and return a struct where some fields are &'a str which are just bytes borrowed from that buffer. You could then perhaps trim() them - returning another &'a str which just points to a smaller slice of that same buffer, and pass them off to other parts of your program - potentially even pass them to an async function that you await on, which could run in another thread - and the explicit lifetimes mean your code will fail to compile if the buffer can be dropped before the string is.
I'm not a proficient Rust programmer yet, but the thing I've been finding is that previous programming experience often suggest solutions that require lifetimes. These solutions are often inappropriate. I think as one becomes more fluent in Rust oriented solutions, it will become more obvious that there is a better way to approach the problem. It's not so much that the use of a lifetime is indicative of poor code. It's more that due to lack of experience in the Rust way of doing it, you are more likely to reach for lifetime solutions that are inappropriate. Trying to avoid them at all cost as a novice seems like a good way of finding out where you absolutely need them (and, in my experience, there are definitely many such places). It sounds like a good exercise.
Consider a function that takes two references and returns a reference. Which of the two input reference lifetimes does the lifetime of the returned reference live in? Trick question, the function returns a static lifetime reference to a constant.
Is this an anti-pattern? There are many situations where this would be the most efficient implementation. The compiler can frequently elide lifetimes, it only needs them when it's ambiguous. Sometimes you need to pass two references and return a reference.
I think the question for me (in this thought experiment) will be "what will the code look like if I try to avoid this?". And what are the repercussions? From a performance and/or readability / comprehension standpoint (or any other perspective).
Completely pointless tangent: the article uses `unwrap`ping as the -ing form of `unwrap`. That looks weird to me. It occurs to me that `X`->`X`ing is a better rule because of things like `leave`ing (not `leav`ing).
My guess is that they used that spelling because "unwrapping" is a real word, and that's how it's spelled.
Tangentially, the word itself is spelled that way to preserve the vowel sound of "unwrap"; without the second "p", the "a" sound would turn from a short "a" to a long "a". A good heuristic for this sort of thing is that a syllable that ends with a consonant will make a short vowel sound, whereas one that ends with a vowel will have that vowel sound be long.
>My guess is that they used that spelling because "unwrapping" is a real word, and that's how it's spelled.
Yes, obviously, but you're missing OP's point. When the keyword is backticked it doesn't really make sense to adjust its spelling based on the addition of the -ing affix, since in some cases this would also require deletion of letters at the end of the keyword -- which clearly should remain unaltered when backticked. Thus, the only sensible and consistent rule is not to make spelling adjustments when adding -ing to backticked keywords.
As someone who's getting in to Rust, does anyone have suggestions on blogs or people on Twitter to follow that would be good for continuing to push the envelope of my knowledge of Rust? I've got a couple, but I'd love more.
Not a blog or a Twitter, but Jon Gjengset channel on YouTube has awesome live (or records of) coding videos on Rust.
The topics are not for beginners, but even thought I'm starting on Rust as well, I've found his videos an excellent resource.
What a great question! Because it's implemented as a const/codegen feature I'm not sure what it would mean to support it for proc macros. Probably gnarly design/RFC work?
can I poll the audience of seasoned rust devs: how do you deal with the borrow checker and lifetimes? do you simulate both in your head before writing your code or do you just let the compiler yell at you and then silence it by fixing bugs?
I ask because I've written some trivial rust code (a little TUI client for a database) and while it was mostly fine, I didn't feel myself absorbing the rules by osmosis (I had to wait for the compiler to yell at me and then wrestle).
I'm not sure if it's a simulation so much as the rules just become normal constraints you use when writing code. It is super rare for me these days to write any line of Rust code that makes the borrow checker complain in a way that surprises me. It took me a while to get to that point. I don't remember how long though.
Not really. Pick a project and start building. At least, that's my learning style. Reading things like the linked list book is great, but eventually, you probably have to put it to practice in order to really learn it (like most things).
I would like to second everything you've said here, and add a few things:
1. If you are feeling stuck, please reach out on users.rust-lang.org or discord.gg/rust-lang. We're here to help!
2. If you're feeling really stuck, maybe take a break and come back to Rust in a few months. A number of people have given up on Rust in frustration, came back after a significant amount of time, and then said "why did I think that was so hard before?"
3. Don't worry about a few calls to clone when you're getting started; it's better to have a working program that does some extra copying than it is to not have a program at all.
4. Especially when starting out, you almost always want structs to own their data. Don't use references as struct members unless you're absolutely sure that's what you want. Advice #3 helps with this.
5. I think one of the biggest mindsets that can set you up for failure with Rust is "The compiler says no, how do I get it to do what I want anyway" instead of "The compiler says no, what is it telling me, and how can I work with it instead?" Especially if you come from an OO heavy background, you may need to change the patterns that you reach for initially. Yes, you can write any style in any language, but Rust pushes you towards its idioms much more strongly than other languages do.
The Rust channels on Matrix and oftc irc are really helpful as well :)
For drills, I recommend exercism.io in practice mode (mentor/student ratio is pretty bad). After working on your own solution for a while, then checking how the others solved it... priceless. Somebody almost always figured out an objectively better solution.
The key, I think, is to figure out which kinds of Rust patterns are ambitious and efficient if they work, versus patterns that are easy to write and unlikely to fail. It's tempting to use references everywhere possible for efficiency, but Rc, Arc, Cell, RefCell, Mutex, and other smart pointer types can get you out of a jam. Sometimes you just have to bite the bullet and clone things until you've figured out a better way.
Looking back at the very first Rust module I wrote, it's actually not half bad. I thought it was bad at the time because I was continuously fighting the borrow checker, but now I see that the borrow checker led me toward fairly idiomatic Rust patterns.
BTW, if you're coming from Javascript or Python, the closest thing to a JS/Python string in Rust is Rc<String> or Arc<String> (depending on whether your code is multithreaded.) I don't want to admit how long it took me to figure that out and I think that little bit of wisdom ought to be featured prominently in the Rust book. :-)
Perhaps. I can think of a couple reasons to steer people away from Rc/Arc: they are less efficient than references and they don't support cyclic garbage collection. On the other hand, Rc/Arc are very helpful for newcomers coming from garbage collected languages and they can be more efficient than cloning. If I asked an employee to convert some high level module to Rust and that employee used a lot of Arcs where they weren't really necessary, I would say that employee had done a reasonably good job.
Writing rust since ~2016, doing a lot of async/await and multithreaded stuff and using explicit lifetimes quite often.
At the beginning it was a case of write the code, then see how the borrow checker complains and fix it. Now that I understand the semantics intuitively, for the last year or so, it's very rare for the compiler to complain about anything. I guess I've internalised the semantics -- they're not exactly arbitrary, mostly the lifetime rules just follow from what you should be doing anyway.
One thing I would encourage (and this applies in general) is trying to understand _why_ the compiler is complaining about some code you wrote before attempting to "make the error go away". Don't just hammer away until you make it work; that's not how you learn. I've seen a lot of Rust code that's full of unnecessary use of Cell, Rc, etc because people didn't take the time to understand the semantics and just reached for ways to "make the errors go away".
Lifetimes are not actually that hard: it's mostly stack discipline, with some escape hatches. The compiler yelling at me is mostly stupid, surface syntax like things, but that's because I've learnt naturally to structure my code in a way that makes sense with stack discipline.
Over 5 years of Rust experience, 2 of which are professional.
I honestly haven't felt like it was much of a problem. IMO, the main thing to understand is that Rust strongly prefers that each instance has one and only one owner at any time, and any references to that object should only exist for a strictly constrained amount of time. This is the preferred style, and doing anything else, including some patterns common in other languages and OOP, will be very painful. Don't be afraid to throw clone() around with reckless abandon any time references get confusing, most things are cheap and fast to clone, and it's generally better to get your program working now and then optimize as needed, rather than try to figure out how to handle a weird case.
Before rust, I would incrementally evolve an incomplete design into a complete design (building a tree by building leaves, branches, and a trunk and then assembling them.) In rust, my early incremental versions would have lifetime issues that I used to not worry about until later. Now I start with a very small complete version that I make bigger (building a tree by increasing the size of a sapling.)
The long term plan is to make assert! smart enough to recognise the assertion and provide good error messages based on its shape, maybe eventually deprecating assert_eq! etc.
I don’t really like that procedural macros are a thing. Rust isn’t quite at the Go level of forcing style, but it’s closer to it than it is to C++ or C, and I think the ability to implement DSLs with procedural macros makes it harder to look at code and understand what it does.
I guess I’d rather have the feature for when it’s truly necessary than exclude it from the language entirely, and just hope that people use it with forethought.
No procedural macros means that libraries like serde and diesel cannot exist. Or at least, they would have to exist in a much worse form. This was true before Rust 1.15, when the first parts of procedural macros were stabilized. I remember the release specifically because things got so much better after this was possible.
FWIW, I chose to learn Rust specifically because of the metaprogramming afforded by procedural macros. The code I write leans heavily on (de)serialization, frequent error handling, declarative programming, and other automation. I can hardly imagine how verbose my code would be without procedural macros.
One of the first things I wrote was a procedural macro. Yeah, I like to learn things the hard way, but I'm glad I did! Once I got past the confusing split between proc_macro::TokenStream and proc_macro2::TokenStream, I found that using "syn" and "quote!" is really quite enjoyable.
A macro is a bug report against the language; Rust is crying out for a way to treat structs generically (i.e. what frunk does with LabelledGeneric), but that only needs to be implemented once. There's no reason serde and diesel couldn't be built on top of frunk (that's how many things work in the Scala world: specific libraries like circe or pureconfig don't write custom macros, they just pull in a dependency on Shapeless) - and then once there was a clear consensus on the right way to do it, LabelledGeneric or equivalent could be made a standard part of the language and no macros would be needed at all.
Maybe macros make sense as a pragmatic way to allow prototyping of future language features. But language designers can, and IMO should, work towards making them unnecessary in the production-code ecosystem, because the disadvantages for readability and tooling are very real. And that's only going to get worse as more advanced tooling (IDEs, profilers etc.) becomes available and people rely more heavily on those tools to be deeply integrated with the language, because those tools are never going to be able to understand ad-hoc macros.
>Rust is crying out for a way to treat structs generically (i.e. what frunk does with LabelledGeneric), but that only needs to be implemented once. There's no reason serde and diesel couldn't be built on top of frunk [...]
What frunk does at runtime (well, its proc macros are still compile-time, but the code that makes use of the metadata is runtime), syn does at compile-time. I can understand that it's nice to have a simpler model where everything happens at runtime, but the things that proc macros can do include things that matter to the typesystem. For example, some of the macros I've written generate new types based on the input tts. You can't do that at runtime.
>And that's only going to get worse as more advanced tooling (IDEs, profilers etc.) becomes available and people rely more heavily on those tools to be deeply integrated with the language, because those tools are never going to be able to understand ad-hoc macros.
The mainstream tools (rls and rust-analyzer) get their information from the compiler, and thus have access to the expansion of the macro rather than just the macro invocation.
> What frunk does at runtime (well, its proc macros are still compile-time, but the code that makes use of the metadata is runtime), syn does at compile-time. I can understand that it's nice to have a simpler model where everything happens at runtime, but the things that proc macros can do include things that matter to the typesystem. For example, some of the macros I've written generate new types based on the input tts. You can't do that at runtime.
Again, though, that's something you want to be able to do in a standard first-class way in the language, rather than needing a custom macro. Depending on the use case you either want higher-kinded types (for transformations like producing a "patch" version for a given record, where all members are (recursively) optional) or dependent types (for transformations where you really just want to do a bunch of specific type->type cases). It's really not rocket science - Scala has been doing this stuff for years already.
> The mainstream tools (rls and rust-analyzer) get their information from the compiler, and thus have access to the expansion of the macro rather than just the macro invocation.
Sure, and that helps, but the tool can't possibly know what the full relation between input and output is (because by definition the macro is an arbitrary function), so it's never going to be as reliable as first-class code.
>but the tool can't possibly know what the full relation between input and output is (because by definition the macro is an arbitrary function)
The macro can associate output tokens with the spans of input tokens, either by reusing the input tokens (`#[derive(Foo)] struct S;` -> `impl Foo for S`, the `S` is reused), or using the macro API which allows specifying an arbitrary span for new tokens. This is required both for ident hygiene and for associating errors with the source code, thus it is a core part of the macro API. And since the compiler has this information, tools have it too.
That means if you have
#[foo]
struct S; // (1)
let s = S; // (2)
and use an IDE to refactor-rename `S` to `S2` at the line marked (2), the IDE has enough information to know that it must also rename the `S` at the line marked (1), even though it's a macro input. This applies equally well to function-style proc macros `foo! { S }`
Agreed. But in order to receive that bug report, you need a macro system. This is partially because...
> Rust is crying out for a way to treat structs generically (i.e. what frunk does with LabelledGeneric)
frunk is very cool, and I'm glad it exists, but it gets about 100 downloads a week. The matches macro that we uplifted to the standard library in this release is getting around 20,000.
> the disadvantages for readability and tooling are very real
Interestingly enough, just this week rust-analyzer gained the ability to do code completion within macro invocations.
> frunk is very cool, and I'm glad it exists, but it gets about 100 downloads a week. The matches macro that we uplifted to the standard library in this release is getting around 20,000.
Indeed, because library authors find it easier to write their own macro to solve the problem. And maybe part of that is that frunk is too hard, but I do think it's also the case that macros are too easy - or rather, that the ease of adding them is not proportionate to the ease of maintaining codebases that use them. http://www.haskellforall.com/2016/04/worst-practices-should-...
Sure it can be misused, but honestly, all the macros I've seen so far make sense that they are macros and they make things easier to read and write.
I'd rather have a small simple concise 1-liner, than being forced to write 20 lines of code otherwise.
In Go you have `if err != nil` on every other line it seems and that makes things hard to read. The `try!` macro which later became the `?` makes things much easier to read IMO. Especially when you have to drill into something nested.