All really good things to mention, but the data races thing is a big deal and gets just a short reference and where is the bit about the borrow checker?
The way I see it the things mentioned are nice appetizers and the data race and borrow checker are the main meal.
IME the most frustrating problems are not that you forgot to exhaust the switch statement or didn't initialise a new field, it's when you get a segfault that's hard to reproduce and you have barely a hint about what's caused it.
For my first C class in uni I wrote a curses-style CLI spreadsheet tool with a csv parser, reverse cell reference table, automatic modification-based recursive recalculation, a basic formula parser, an infix notation to reverse polish notation "compiler", and a stack-based RPN evaluator (I may have gone a bit overboard on that assignment). The one multi-user unix server for all students that I wrote it on (named 'foley', some CS reference I think) had an ancient gcc compiler that compiled it just fine, but when run it would segfault. Hardly surprising, I created hundreds of segfaults, but this one would only segfault if I used -O0 and didn't add any logging statements. That's right, it ran fine with any level of optimization above 0 or if I used any logging statements, and only segfaulted on -O0. Utterly infuriating and flagrantly defied my expectations. I got help from some patient soul I found on freenode ##c (I wish I could remember their name) who didn't see any obvious issue or warning and couldn't reproduce any segfault with their much more recent compiler. -Wall+++, asan, nothing cracked it. Everything else was exhausted: it must be the compiler. But I couldn't just update the compiler package because it was a multi-user server and I had zero permissions, so I ended up bootstrapping a 10-years newer gcc through multiple intermediate versions and installing it in my profile (quotas be damned) and used that. It didn't segfault on -O0 anymore. I haven't voluntarily used C since.
This is not a good anecdote - You hit a 10 year old bug because your sysadmin was too lazy to update gcc and then formed a bad impression of the software?
Guess what, if you install a 10 year old version rust none of your code would not compile at all. Your logic is so flawed its hard to believe you are being serious.
If anything it's impressive that a 10 year old C compiler worked most of the time when most other languages are not stable on these timescales.
The oldest rustc version I target (for a tiny project, admittedly) is over 6 years old, and it's fine. When code works it also works on the latest release. When code doesn't work I get a reasonable compile-time error message, not a mysterious segfault at runtime.
It's not easy, even in a few hundred lines I had to remove many conveniences to go that far back, but it's very legible. The compiler has my back, I'm never confused about whose fault it is.
Rust is young, but I don't expect this to be any different in 2030.
That's not related to C, except in the sense that GCC is written in C. GCC was just emitting incorrect code because it had a bug in its code generator. If there's a bug in the LLVM code generator (which is written in C++), rustc might end up doing the same thing.
If you write the compiler in Rust it could still happen. Rust code (that compiles) has less bugs than the corresponding C but not zero bugs.
It doesn't sound like you went overboard, it sounds like a good project. Maybe you should have written it in Perl 5.001 though.
Because it was C I couldn't be sure it was the compiler's fault until I really exhausted every other option. Maybe spent 100h between a few people getting to the end of it. If I had written in safe Rust (I would have, I wasn't doing anything weird) and it segfaulted I'd know immediately that the compiler was the problem because you can't make safe rust segfault.
Or he could do the normal thing and run valgrind to instantly see exactly where the problem is. I don't even disagree with the premise, but these are straw man arguments.
Valgrind didn't exist then. (Next you're going to say ASan and UBSan. Those didn't exist either.) They (he?) could have run GDB and disassembled the code around the crashing instruction and verified that the machine code there didn't make any fucking sense. But that implies they knew assembly language well enough to recognize when their compiler was spewing nonsense, which is a couple of steps above "can read working assembly emitted by the compiler" or even "can write applications in assembly language".
Even with Valgrind it can be tough to distinguish whether your output code is wrong because of a bug in the compiler or because your code was just wrong. There was a change to GCC a few years back that introduced security holes into the FreeBSD and NetBSD kernel because it just threw away code that would only be invoked in "undefined behavior" scenarios. Signed integer overflow I think.
You are unfairly comparing ancient C with modern rust - Try comparing modern C with modern rust to avoid making a straw man argument. Like I said, I don't even disagree, I still think you are just doing everyone a disservice with this line of reasoning.
He said if he had used rust - I am saying if he had used C when rust was available 1. The bug would have been fixed. 2. He could have used valgrind. Modern C also has something called ubsan, and another thing called frama-C.
These tools may be inferior to what rust has, but ignoring they exist or comparing 10+ year old C with modern rust is a bad faith argument.
I agree, I don't think that a bug in GCC last millennium is a very strong argument for using Rust instead of C today, though such arguments do exist.
However, I also don't think they particularly help you to distinguish a compiler bug from an error in your understanding of the language semantics, although they sure do help a lot with everyday errors.
With respect to questions of inferiority or superiority, keep in mind that Valgrind and UBSan are only dynamic checkers; they don't help at all with errors that don't occur in your testing. Frama-C is a static checker more similar to Rust's capabilities, but much more limited, but also with cscope-like abilities for reverse-engineering existing source bases.
The great advantage these three tools (and ASan) have is that you don't have to rewrite your C in Rust in order to use them.
I agree because having a type system that directly provides the guarantee that whole classes of runtime error cannot happen provides fast feedback during development at a low cost.
I disagree because even in a press button (+ tuning) approach, you can prove things with Frama-C that the Rust compiler cannot prove (reason why there is runtime bound-checking, implementation defined behavior for integer overflow and so on in Rust). But also because you can prove much more advance properties than "just" absence of runtime errors.
I don't see any inconsistency. Universities still have multi-user unix servers for students in 2022 - at Bordeaux there's a few 64/128 thread ones with as many gigabytes of RAM, which leaves a lot of room for doing work during class. (And most likely they would be running some Debian LTS like Jessie or Stretch)
Sure, I'm using a multi-user Unix server right now in another window. The key point was that there was just one server for all the undergraduates. (And that they had to worry about a private install of GCC using up their disk quota.)
Yeah, you can't build modern GCC inside the 1GB quota, but you can probably build it in /tmp or /var/tmp or /scratch or something. The actual built compiler is under 50 MB in GCC 8. And if you're really cramped on quota you can gzexe it, at the cost of painfully slow startup times.
20 years ago nearly all universities could afford more than a single Unix box for all the undergraduates to share. At that point it was mainstream to buy a cheap US$300 ATX box and run Linux on it, so you could have one per student or one per dozen students, rather than one for the whole undergraduate student body.
The Rio Receiver came out 21 years ago. That was a Linux box sold as a consumer electronics stereo component; it grabbed MP3s over Ethernet to play them. I don't remember how much it cost but it was less than US$300.
But apparently the original author tried using asan? That seems like an inconsistency in the story.
Valgrind only outputs errors on codepaths that got run. Which sure, if it is a deterministic segfault it would catch, but let’s say you have a race condition and only get segfault 1 times out of 100, and it may not even happen on valgrind?
Yes perl may have been a better choice for the projects sake, but alas the course was apparently a trial-by-~fire~segfault of C + string processing. Loved PCRE though... hmm I wonder if you could do it with just a regex...
I will be very surprised if you can write a working spreadsheet in a PCRE regex. Perl itself permits you to embed arbitrary Perl code in a "regex", though.
There are small parts of GCC that are written in C++, but at the time when universities had "one multi-user unix server for all students" it was 100% C.
I had pegged it as between 20 and 30 years ago, but someone pointed out that the author said they tried using asan. 40 years ago most universities didn't have Unix boxes accessible to random undergraduates at all.
In addition, most of these checks are provided in other languages or linters for those languages. E.g. C++ has had RAII before Rust has existed. C++ (with -Werror), TypeScript and most FP languages have exhaustive switch checking. Clang can catch many (but not all) initialization errors. The places where Rust adds value (data races, memory safety, explicit unsafe) are not discussed in the article.
Rust also adds value in providing so many of th checks where most languages only have a subset, and being incredibly consistent in having safe defaults.
Not untrue, but the post doesn’t mention prior work, so it’s implying that Rust is one of the only languages that has these features, whereas even C++ (notorious for being unsafe) as a majority of the features (in Clang). Clang can even catch many data race issues with the proper annotations. Maybe it’s ignorance, but given how posts proselytizing Rust tend to get popular on HN, it makes me suspicious that the omission was intentional.
> In addition, most of these checks are provided in other languages or linters for those languages.
Linters are inferior to things the compiler catches for the same reason test cases are inferior: They are not enforced. If the compiler doesn't allow something you can say with confidence you will never see it in the wild. With linters, compiler switches (-Werror) or test cases you'll have no idea.
I would like to see more about atypical language protections.
GCC has many underknown flags that protects against very specific things.
Kotlin is the first popular language to support structured concurrency, eliminating many classes of concurrency errors.
It's basically "RAII" for threads, coroutines.
Tailrecursive and deeprecursive allow for making any recursive function stack-overflow safe.
I've seen people who state their love for Rust and then fail to explain the difference between passing an argument by value, reference or mutable reference.
I mean those are two separate things. Depending on how you code, Rust can be similarly high level as Python, make it much easier to design with types than C++ and has great package management with cargo.
You can find plenty of reasons to love Rust, without even getting to the technical details.
Not really, once you start something more complicated than hello world and start to try mutate things for example it get 100x more complicated that Python.
In fact, Rust is MORE high level than python and most languages. That is exactly the borrow checker and all the other things are.
What is not, is being "simple" as python.
I code now full Rust and is the most productive language in my +20 years of this. My other picks: python, Delphi, FoxPro.
But is not productive exactly the same way python/delphi is. It has other axes of it. So in some task Python is unmatched and my main pick for scripting or brainstorming stuff. But Rust is the best at being overall productive.
Where is not (today) is lack of time/resources to be there (like GUIs. But honestly, nothing compare to FoxPro/delphi)...
Amen. Every so often I boot up Lazarus and lament what could have been. Even the best web-dev frontend tools and frameworks do not come close.
I wonder if there is even a market for those sorts of tools today, though. I wasn't there for the decline of Delphi (and FoxPro, et al), so I've no real reference as to why they declined.
I think all the stuff about nocode/lowcode/jupyter/etc say exist the market, but is going in a overly weird route, as if nobody know what this tools can do.
No, this low-high level axis is not really “permeable”. You can maybe get as productive in rust as in some managed language at the first time you write the program. But later maintenance will simply drop your productivity as any refactor will likely alter your whole memory/ownership model and cause much more refactor recursively. Sure, you may say that “but it will be correct in the end while in python you have to hunt down the newly created bugs” and you might be right, but it is not a small price to pay. Managed languages let you get away with many many unanswered questions that you have to meticulously answer in case of low level ones, and I think it is a fair tradeoff depending on use-case. But as much as I love Rust, I won’t choose it for some database-mangling web service.
> But later maintenance will simply drop your productivity as any refactor
This is very off weird take: Refactoring in Rust is one of their strengths as language (a major one!) where even VERY deep changes can be done in economical times.
Try the same in python is nearly impossible.
And I mean it: I fully rewritten all my app (a fairly complex niche eCommerce platform) to async and remade all things along it.
And I have A LOT of experience building, rewriting, and porting code; and Rust is the faster/more friendly to refactor both large or small in all my years.
However, I concede that get the knowledge with Rust have challenges and my first attempts at this (when doing my hobby https://tablam.org language) were miserable by lack of understanding, so it could be a factor.
That's a valid criticism. While I can be much more productive in .NET with the help of the language, the compiler, the IDE, intellisense and copilot, looking at last Techempower benchmarks, Rust is still 1.6 faster.
I would use Rust for Web if I'd have to squeeze every bit of performance. Most of the of the time I don't have such need for performance.
This is what I mean, I ported some Python code almost 1:1.
If you program in a functional way anyway, i.e. having data go through a series of filters, it is quite easy. But not all ways of programming are so easy to port to Rust.
> start to try mutate things for example it get 100x more complicated that Python.
I don't know about 100x, using Box and mutexes it is pretty much as complicated or maybe 2x. If you want it to be efficient and leverage all of Rust then sure, but that wasn't what they were meaning.
One tiny program does not a realistic comparison make, of course. There’s tons of situations where either language can make things more complex than this example. But for basic tasks it’s not particularly complicated.
For reference I'd say the "current async framework" is either Starlette [1] (low level) or FastAPI [2] (high level) for web stuff, and trio [3] for more stupidproof general async than twisted. IMHO of course!
I'm not sour about people enjoying things. Would you not feel peculiar about people who are really enthusiastic about a sous vide machine and use it to heat water for their tea? And when asked about their favourite cut of steak they like to sous vide, they'd stare blankly and proclaim that they are vegan? There's nothing wrong with using tools any which way you want, but the reason d'entre for Rust is fearless concurrency, and the distinction between borrowing and moving, with the addition of Send and Sync marker traits, is kind of the whole point of the langauge. I agree that it could still be a good language without these, but if it wasn't for these features, I doubt there'd be nearly as much enthusiasm behind it.
Rust is slightly unusual in that most users/potential users of it tend to be very vocal about it. It would be nice if their knowledge matched their enthusiasm.
like that's how you get the knowledge, you're enthusiastic about a thing and then spend a bunch of time with it and then you build up knowledge. it's healthier in that order.
> For all the other scenarios of distributed computing or OS IPC, it does very little.
It might be worth clarifying that catching data races isn't just about catching race-conditions-which-are-probably-bugs. Data races are full-blown undefined behavior in the C/C++/Rust memory models, which can (not necessarily frequently but under the right conditions) corrupt memory just as badly as a use-after-free or an out-of-bounds write.
Agreed, however when talking about how great Rust deals with those, we should not forget it is only one use case among many others where Rust currently doesn't do any better than the competition.
Specially if we take into consideration that focusing back into processes is the only solution to prevent certain exploits.
> It may sound stupid, but you can't have unhandled exceptions if you don't have exceptions...
> panic!() exists in Rust, but that's not how recoverable errors are handled.
This is the worst argument in the whole article, and this is the worst part of the language. Everyone says it's not like exceptions, but in fact it is much worse. Panic is stringly typed and you can catch_unwind it, just like with try/catch in any other language. And the actual worst part of it, you will never know if a panic can occur in any of the underlying functions until it is too late. Developers be damned if they want to choose different behaviour other than crashing the whole program.
Either double down on using the standard error handling everywhere, or put something like "throws panic" in the function signature (ala Java checked exceptions). Many parts of the language has strict checks for everything, why does panic has to be an outlier?
It's not like exceptions because it's not used like exceptions. You only use panic if you want to crash the whole program. If you don't want to crash the whole program, you don't use panic. You do not want to crash the whole program if the user data failed to validate, so you do not panic in that case. If a library panics on invalid user data, that's a pretty serious bug.
I've been programming in Rust since it came out, and a couple of those years professionally, and I don't think I've ever seen anyone use catch_unwind. Maybe once in a test case?
To be concrete, let's talk about an example of a panic. Say you want to access the 3rd element of a vector. There are two cases:
1. You're not sure whether the vector actually has three elements on not. In this case, you call `my_vector.get(2)`, which returns an Option, and you handle the case where it's present and the case where it's not. This is standard error handling.
2. You are sure that the vector has at least three elements. Perhaps you just checked its length for some other reason, or you are careful to maintain this invariant, or you just constructed this vector by pushing 5 elements onto it. In this case, you would typically use `my_vector[2]`, which panics if the vector is too short.
For #2, the thing to notice is that this function literally never panics, under any input whatsoever if it is written correctly. Should that fact really clutter up its type signature, either by forcing it to return a Result type or by forcing it to have a "throws panic" marker?
EDIT: This is for a function that uses a possibly-panicking operation, `my_vector[2]`. There are also the functions that define a potentially panicking operation, like the vector indexing function itself. You could put a marker in the type signature of those, that would be reasonable. Though it would only be for users; the compiler wouldn't care.
Wasn't this argument used all the time by the Go community. I.e. only use panic when you intend the program to halt, and handle all potential problems with the Error type.
I think Rob Pike even said it's easy to see where a program fail in one of his talks?
But to me the superb thing about exceptions, is that error handling can be done where it makes sense. I.e. we can try{ problem-code }catch(problem){ handle problem } in a single location. Otherwise we end up peppering the entire code base with a ton of error checking far down the call stack, where we really cannot do much about the problem anyway (unless we are writing command line tools where error handling is just writing the problem to stderr).
Exceptions gives us a nice way to let problems bubble up to the surface, while also stating what the problem was, and where it occurred. That is great IMO.
> Exceptions gives us a nice way to let problems bubble up to the surface, while also stating what the problem was, and where it occurred.
Work is ongoing on some of this, but there are popular libraries in Rust (like `anyhow`) that let you attach backtraces to regular errors, add context, etc. Propagating erros to callers is handled with the standard `?` operator, which means "short-circuit and return this if it was an error, otherwise give me the successful result". This has the benefit of making early exits explicit, without interrupting the visual flow of straight-line code.
The simple explanation is that Rust has an equivalent of Java-style exceptions of "throw here, handle elsewhere", but has a different syntax for this. Instead of try/catch, there's a `?` operator to return ("rethrow") the error to an outer scope. It's a better fit for Rust's use of a generic Result type, but overall its usage is similar to the checked exceptions in Java.
Because Rust uses the type-safe explicit Result/? approach for all non-bug failures in the program, the implicit panic (that behaves similarly to RuntimeException in Java) is reserved for assertion failures and crashes only.
`catch_unwind` is not guaranteed to work in Rust. There's a setting to disable it and always hard abort() the whole process on every panic. Rust is serious with panics being for programmer's bugs only, and not trivialities like "file not found".
Thanks for the explanation. I guess most languages need this feature i.e. fail anywhere below this call, then return the type error + where it occurred + a stack trace. I've even heard of people doing stuff like that in C, where they store the stack trace in a list they populate and return errors as part of the same struct, in order to have something similar to exceptions.
I have to say I really enjoyed Rust when it was in its infancy (version 0.1 - 0.2 or thereabouts), but have since fallen off. It used to be so simple and so clean, and unlike anything else. Today it's just way to complex for me :-)
> But to me the superb thing about exceptions, is that error handling can be done where it makes sense.
Not necessarily. With exceptions, it is easy to be a cause of error and just throw the exception, then expect up the stack to handle it. Which of course has no idea how, it didn't control the cause in the first place.
Forcing error handling as near as where error can happen prevents this.
Actually, up the stack is usually the only place that knows how to handle the error. For instance sometimes dumping to stderr is the right thing to do, other times logging it, other times displaying a generic crash GUI, sometimes display a customized UI. There may also be times when the exception can be handled in a better way, with fallbacks for example.
The Rust/Go approach always makes me laugh. Normally in engineering or anything where reliability matters, panicking is understood to be a bad thing to do and people go through extensive training to ensure they don't do it. Somehow these language communities decided that panicking and giving up on the spot is a smart behaviour.
Panic is idiomatic error handling. Take something as basic as indexing into a list. Get it wrong and Rust will panic.
Sometimes exiting the program is the right thing to do.
Yes, but it's very rare that the code where something went wrong is in the position to decide that. The survival of the entire process is not a decision to delegate to every possible line of code or library author.
Consider a very common case where I benefit from exceptions every day - my IDE. IntelliJ hosts a bazillion plugins, of varying quality. These plugins like to do very complex analysis on sometimes broken and incoherent snippets of code, that may be in the process of being edited. In other words it's a nightmare to correctly predict everything that can go wrong.
Not surprisingly, sometimes these plugins crash. And you know what? It doesn't matter. A lot of the code is just providing optional quality-of-life features like static analysis. If one of them goes wrong, IntelliJ looks at the exception and figures out which plugin is likely to blame, it examines the type of error and maybe gathers editor context, it can report it easily to a central server that then groups and aggregates exceptions based on stack traces. Meanwhile as a user, it doesn't bother me because it's fine to just not have that analysis show up in the editor.
If every time an IDE plugin encountered an unexpected situation it aborted the entire process it'd be insane. The plugin ecosystem could never scale that way. People would be afraid of installing/upgrading plugins and that in turn would discourage people from writing them or adding features to them.
In reality nothing does that because, well, why would you when you have good exceptions? But even so, Java has a way to block that using the SecurityManager. Now they are deprecating the SecurityManager "how do I stop code calling System.exit" is one of the use cases they're planning replacements for.
I'm sure there would still be ways to bring the entire process to halt(for example, spawn thousands of threads with infinite loop). My point is just because a bad developer wrote bad code doesn't mean that a tradeoff chosen for a language design is necessarily bad.
In reality it's very hard to accidentally write an infinite loop that spawns threads. There's no idiom that would lead to such a pattern and I can't recall ever encountering such a bug in the wild.
Yes, in theory, there are all sorts of ways you can still trash the process with bad code. But in practice, the sorts of bugs that programmers really make in GC-d memory-safe languages are the ones that don't. So, exception based error handling really does come in very useful and Rust probably got it wrong here.
> The survival of the entire process is not a decision to delegate to every possible line of code or library author.
Stated like that, who can really disagree?
I remember when I was writing a bunch of Go when the language was still very new (2009 - 2011). One of the most popular use cases for the language was making websites. All sorts of unexpected problems caused the entire website to go down, due to unexpected panics here and there. The suggested solution from the Go team was to just restart the web-server whenever it was killed by a panic. Surely that cannot be the best way to do it..
>Panic is idiomatic error handling. Take something as basic as indexing into a list. Get it wrong and Rust will panic.
This is not really true. If you are indexing into something that may fail you use the `get` method which returns an `Option` if the index is out of bounds. The index operator is just a shorthand for `v.get(i).unwrap()` pretty much.
Yes, but the problem is that very often a programmer "knows" an index operation can't fail because they haven't thought of a case where it is a different size, or code gets refactored and assumptions are invalidated, etc.
The panic mentality comes from people who have spent most of their life writing C++, in which if anything goes wrong like an out of bounds index, memory might be corrupted in arbitrary ways, and in which you don't have a GC to clean up after you. Writing exception safe code is much easier in type safe GCd languages, and many programming errors end up being recoverable.
> it is easy to be a cause of error and just throw the exception, then expect up the stack to handle it.
Agree, this can happen. Perhaps the bad attempt at fixing this in Java for instance - checked exceptions, made people dislike exceptions ever more. The caller "has to handle" the exceptions or re-throw them of course. Even though RuntimeException's can come from anywhere at anytime, so "guard" provided by checked exceptions just made a complete mess of things. People are lulled into thinking that methods without the 'throws BlaBlaException' signature are safe and so on.
I guess no language is 100% on everything, but I've always felt that exceptions are one thing I really like; especially when a language manages to do them correctly.
I don't understand your #2 (or your whole point). It's exactly the case for exceptions and how exceptions happen. "You won't get an exception if your code is written correctly and the inputs of your program match your programmer expectations" yeah maybe two year down the line someone refactors the code which was resizing the vector before and now you have the most run-of-the-mill exception ; I just hope that someone making an app with your library has a way to catch the panic so that the software doesn't crash but shows a helpful error dialog to your user and makes a backup of its data before softly existing instead, otherwise we're really back at the pre-1980 state of the art of software design
> maybe two year down the line someone refactors the code which was resizing the vector before and now you have the most run-of-the-mill exception
In other words: there’s a bug in the code, and that bug has now caused an unrecoverable error, panicking the thread. Now the thread has died (or maybe caught the panic to present a friendly error message). Either way, the user is now aware of the bug, and disaster has been avoided.
Of all situations where your app might want to create a backup of its state, why would you choose to do so precisely while unwinding a crashed thread, where all assumptions, bets and invariants are already off?
And what would the helpful error dialog even say? „A problem has occurred and the app will now shut down“? From the user’s point of view, is that really an actionable or helpful error dialog?
> And what would the helpful error dialog even say? „A problem has occurred and the app will now shut down“? From the user’s point of view, is that really an actionable or helpful error dialog?
Yes, literally. This is already much better than anything that gets the spinning ball of death of macOS going. You can even continue if you are running an event-driven app where the error may have happened as part of an event handler (and thus limited to a very specific part of the software).
To give my own experience: I develop https://ossia.io and use this catch-all method. In 7 years I've gotten numerous thanks from the users for it not crashing but being able to carry forward in case some issue crops up in a sub-sub-sub-module. Not a single time I remember this to cause some memory corruption later.
(backing up state is done up to the previous user action but while in my case it works, it's not always practical)
So in this space, you might well feel confident that catch_unwind() is appropriate, although I still think the thread solution is more elegant.
I suspect in reality most of the problems this would catch in OSSIA wouldn't end up as panics in a hypothetical "Rust OSSIA" because of the different attitudes to exception throwing/ panic vs "normal" error flow in these languages and libraries - unless you got really happy slapping "unwrap()" on things when you shouldn't, but sure, it would solve this problem.
As to memory corruption - the problem isn't strictly "memory corruption" but unstable system state. If my underlying cause is that somebody's dubious Leslie simulator blows up when I frob the gain control on it too quickly, restoring exactly the state in which it blew up last time doesn't help me on its own. I need some way to say OK, that was crazy, no Leslie simulator until I save the project and then we can take it gently, which again is somewhere the thread solution is nicer.
>To be concrete, let's talk about an example of a panic. Say you want to access the 3rd element of a vector. There are two cases:
Reality is not that simple, if you worked in this industry you would know. For example I was building a web scraper years ago and the WebView would crash since is C/C++ , instead of doing it's job and show a web page or a broken web page it crashed my entire program,. The solution was to split my program in a parent program and a child program so this bug does not bring my entire thing down, and I can crash the issue and record the bad url that crashes and try again or just skip it.
I would hate to use Rust libraries that would crash my entire program if they for some reason are bugged. In my experience I found bugs in many popular libraries. So in Rust if I import a say library to resize an img and say the img is corrupted and library is shit it will crash my entire program? I would prefer a higher language where I can try=crash the image resize function and if shit goes wrong I can show the user a relevant message , or fallback to some other resizing method.
> I would prefer a higher language where I can try=crash
What you're describing is the `catch_unwind` mechanism that Rust does have. Because panics are implemented with unwinding (by default), you can catch them. But it's not the normal error handling mechanism; it's the "oh god an assert just failed, or we just OOMed or something, who knows, most bets are off" mechanism. If you have a main loop that's sufficiently isolated from individual tasks, such that you think you can do something useful with the fact that one of your tasks just vanished in a puff of smoke, then catching a panic coming out of a task might be a reasonable thing to do. That often makes sense in server code, where your main loop might want to keep trucking, or at least gracefully shut down other connections. But for most library code, the right thing to do is to allow most panics to propagate and crash the caller.
So for example in JS a correct regex can throw exception on some input , so in the places where this can happen we can use a try catch . What do you do in Rust , do you check the return result and on top of that do you try to catch a panic just in case the regex library is bugged ? so you have to implement everywhere 2 error handling methods to be 100% safe? If yes seems more ugly to have to implement 2 error catching ways.
YEs it happen to me many times to hit bugs when working in real world, bugs in image libraries, bugs in regex libraries, bugs in pdf libraries, bugs in html/xml parsers so from my experience working with c/c++ and higher level languages I prefer the higher level languages, less bugs, almost no complete crashes and better error reports from the exceptions. I never had the tiem to try Rust but I am not tempted so far.
> What do you do in Rust , do you check the return result and on top of that do you try to catch a panic just in case the regex library is bugged ?
Nope, we just check the return result because libraries usually don't crash and have well-defined error cases. Having a decent type system helps catching all the possible outcomes. In a few years coding Rust, i never had a single crash due to a library panic, only from explicit unwraps i applied in my own codebase.
Panics are not intended for errors, but for unrecoverable failures. For example, in rust std a failing memory allocation will crash your whole program, which is in most cases what you want to do. For the remaining cases, there are other non-fallible methods.
For example: String::reserve vs String::try_reserve or HashMap::insert vs HashMap::try_insert.
There is no 100% safe anywhere. Does your JS code handle out of memory errors with try-catch? No, it will abort as if nothing happened at all.
Sure, there are bugs in every code but unexpectedly panicking is considered a bug in a library so in my not too extensive experience with rust libs, these are not the norm at all. So simply writing code where you yourself don’t panic should give you quite a high chance of not hitting this case ever.
>Sure, there are bugs in every code but unexpectedly panicking is considered a bug
Yes so would you like say Firefox to just crash when one of it's many dependencies crashes?
You are suggesting but I am not sure if I understand correctly that only memory errors cause panics? So what if the library reads a file and unexpectedly it shit happens with the file, it will crash the program because the developer maybe forgot to return a special error code in this case,
All the filesystem APIs return Results rather than panicking, since like you said it's expected for those to fail sometimes and for the program to handle those failures. It's possible for a library to convert those Results into panics by calling .unwrap() on them, but that would usually be considered a bad design (ok for tests and tiny programs though). So I think you have an important point here, which is that if your application is calling into a library that you worry might have some bad design decisions in it, you do have to worry about it bringing down your process. And maybe it could make sense in some rare cases to try to isolate that library with catch_unwind. But I think most Rust programmers would prefer to just fix the dependency. The fact that you can visibly spot a lot of these conversions in the code is helpful for auditing.
I'm not super up to speed on JS, but I might draw an analogy to Python. Handling a result in Rust is similar to catching an exception of a known type in Python, a very common thing to do. On the other hand, catch_unwind is (loosely) similar to writing a bare except clause that catches every conceivable exception. You can do that, and sometimes it's correct to do that, but in most cases it's a bad idea. You don't want to accidentally suppress errors that indicate a bug in your program.
Thanks, from my experience with desktop apps in managed language I alwas added a global catch for crashes that were not caught or can't be caught, there I was writing the details in a log file. Then I had a menu entry for submitting a bug report, a popup would open and the user had the option to include the log file with the exception information and details like operating system, runtime version etc. The only thing that was bringing down this app in the higher level language(it was an Adobe AIR app in Action Script 3) was the freaking Web View , because was a wrapper over WebKit and that was C++.
This days doing backend dev I am forced to move stuff in a different process but most stuff I use I prefer to use binaries then libraries , for example for resizing an image instead of using the built in image library that crashes sometimes and brings the script down I install image magic on the server and write a script for resizing an image then call that script and check it's output , sometimes I had to use the timeout linux program to kill the program if it gets stuck on some input file.
If I were to create that image resize library in Rust I would attempt to catch everything , including panics and return it as a error result(so only system crashes would be uncaught)
IO errors are generally handled by returning a `Result` type, that contains the details of the problem on error. You wouldn't use `panic` for IO errors. `panic`s are meant for dealing with broken invariants/assertion failures because of a bug in the program.
You need to run it in a separate process. Rust does not have good enough fault isolation features to safely assume a buggy image processor won’t break your app.
* Entering an infinite loop can bring down everything. A separate thread might not, but since Rust provides no way to kill a thread without it cooperating, there is no way to stop a stuck thread without bringing down the whole process.
* Stack overflow is an instant abort, not a panic.
* Double panic, where panicking calls a destructor that itself panics, is an instant abort.
Question, if you are a library/program author why would you intentionally use a panic and not cleanup and return an error? Maybe I misunderstood and in fact good developers never trigger panics unless there is no way to avoid it, like if they could not prevent it with more checks or is it impossible to cleanup because they already fucked up, wrote garbage in the process memory and safest thing is to kill the process.
I think there are a few cases where Rust likes to panic, but different people probably have different opinions here:
- Extremely common operations with dedicated syntax, where introducing error handling would be burdensome. Things like array indexing or arithmetic overflow. In these cases, you usually want an alternative, fallible way to do the same operation.
- Cases where most callers will probably convert the error into a panic anyway. One example of this might be .split_at() on slices, which is bounds-checked just like an array access. Most callers would probably just .unwrap() the out-of-bounds case, and callers who don't want it to panic can easily check before the call, so it's more ergonomic to panic.
- Cases where the only plausible reason for failure is a bug in the caller. For example, the .borrow() and .borrow_mut() methods on RefCell will panic if a write overlaps with another read or write. The caller is almost always expected to statically guarantee that that doesn't happen, usually by making all borrows short-lived. (And here again there are fallible alternatives available.)
An interesting example of something that doesn't panic, but which probably should, is taking a mutex. The standard mutex in Rust includes a "poisoning" mechanism, which almost every caller just .unwrap()s. I think the majority opinion these days is that poisoning should just be removed, but given that it's around I think most people wish it just panicked instead of returning a Result.
> is it impossible to cleanup because they already fucked up, wrote garbage in the process memory and safest thing is to kill the process
That’s essentially it, yes. My code should never actually panic. If it does, it means the state of the process has become deeply diseased, and attempting to “clean up” is likely to just make things worse. Of course, if it’s safe Rust, then it still won’t write past the end of a buffer or anything disastrous like that, but buggy code is still buggy code and there’s lots of stuff Rust won’t stop you from doing.
One of the more extreme things I’ve done in production Rust code was add a “watchdog thread”. It has a channel that takes unit and receives on a timeout, and the thread doing the actual work is expected to send it a message once a minute. If it doesn’t receive a message within a minute, it hard aborts the process. The default setup is run under a service manager like systemd to make sure it gets restarted, and that failures are actually logged somewhere.
This is meant to solve the problem that safe Rust is a Turing complete language, so is subject to the halting problem. The type checker can prove that you won’t read past the end of a buffer, but it cannot prove that your code will ever actually finish running. Which means, if you have a project like a web scraper that needs high uptime, you need to prevent it from getting stuck somehow.
I agree, so Am I wrong or the issue seems to be community culture thing, where some developers panic too eagerly? Say I make a library for resizing images and I have one public function resizeImage(options) , a good developer would think that maybe my code code has a bug and some function from standard library would panic, I should ensure I catch this and my public function never panics (even if there is no memory,disk or whatever Ias an author I should try not to intentionally panic" where a bad Rust developer thinks like " this will never panic unless I made a bug, if I amde a bug I am happy to panic and crush some sucker program so I get the bug report and fix the bug".
There are always bugs(logic bugs where Rust can't protect you) so why not have a clean interface?
IO errors like running out of disk space would be handled by returning a `Result` type, not by panicking. Often, Rust code/libraries panic on out-of-memory errors because recovering from that isn't a priority for most application code. But if you are writing lower-level or high-reliability code and you do want to handle out-of-memory errors, the Rust standard library (and many third-party libraries) offer alternative fallible memory allocation APIs that return `Result` instead of panicking on out-of-memory.
A good developer will crash the program, as soon as possible, if and only if it has a bug. If you want to write a program that never crashes, then you need to write a program with no bugs.
The reason you don’t want buggy code to limp along after it detects a bug, is that crashing isn’t the worst possible thing.
The worst possible thing is getting stuck in an infinite loop or a deadlock.
> I would hate to use Rust libraries that would crash my entire program if they for some reason are bugged.
You would, but for other programs with other requirements it would actually be beneficial. There's no single right answer, and you should pick the library that follows your particular requirements for that particular program.
You can turn panics into aborts with `panic = "abort"` in Cargo.toml, in which case nobody has to pay for being exception safe (though, to get the full benefits of this, you may have to rebuild the stdlib? I'm not entirely sure here).
I'm talking about the cost paid by the library author for the additional burden of writing exception safe code. Whether you use this downstream doesn't matter, the cost is already paid (in fact arguments like "no one catches" make it worse since the cost is paid and no one benefits).
The only place I've seen people using catch unwind was in Sentry library to catch panics. Needless to say that it was never used even before we removed all unwraps from the code.
> If a library panics on invalid user data, that's a pretty serious bug.
I swear, sometimes it seems like Rust people are from another planet. What do you think "unwrap" does? It's not used in every library, but certainly in many of them.
There’s no need to talk in a condescending manner.
What they said was correct - an “unwrap” outside of test/prototyping is considered a serious bug. They Rust loving strawmen you’re creating never claimed that every line of Rust ever written is perfect and bug free.
So you essentially want me to write error handling code nonstop, constantly, all across my functions. Practically after every 5 lines of code there is going to be an unwrap() where I'm not allowed to call unwrap() so I have to know the details of the implementation, the error code, deal with the error code, return early from the function and then gracefully handle it all. Meanwhile in a language that has exceptions I just put a try catch around all the code I think works fine but maybe not and I deal with it in a single location in a way where I dont have to care about what the precise error code might be.
Error code programming really seems to be objectively worse for everyone except the compiler writer. Somehow people let themselves get convinced that this is better when it's objectively not.
Rust has syntactic sugar to help you coalesce error handling into returning a single Result. You'd have to check and make sure the library you use doesn't call unwrap willy nilly. As crazy as that sounds it is actually common practice in the Rust community, there's tools that reveal use of unwrap and unsafe in your dependencies.
In the end you don't use Rust because it's so easy and nice to use (unless you come from C/C++). You come to Rust because you want meticulous control over performance, and you don't want to sacrifice safety to attain that.
If that's not why you're using it, I agree you're probably better off choosing Java, it's plenty fast and comfortable to use, especially if you pick modern tooling.
> You'd have to check and make sure the library you use doesn't call unwrap willy nilly.
That statement really resonates with me.
If you use a library, you’re responsible for what it does, just like how you’re responsible for your own code.
That’s what the ? syntactic sugar is meant to solve. It will return at the point with an Option null, or the error variant of Result if the preceding expression’s error can be converted to it.
something.map_err(…)? is quite readable in my opinion and that is the worst case, when your method returns a Result<..,..> but the called method has an Optional return type. Otherwise it is just a single ?.
Sure, I do believe that exceptions are superior but we do have to understand that rust is a low-level language, period. It is very expressive considering its nature, but it will never be as productive as a managed language in my opinion - we have this distinction for a very good reason. If you want maximal control over what happens “behind the scenes” you loose some automatism that could improve productivity.
How is try catch any better than Err/Ok pattern? Code that doesn't handle error cases shouldn't even pass any code reviews. This is exactly why Rust guides the programmers in a certain paths to ensure all cases are always handled. If you really don't want check the Err/Ok in each call, you are free to use '?' to pass that burden to higher functions.
They didn’t know about `?`. My guess is that they read the first page about error handling, where it talks about unwrap and match. They didn’t get to the second page, where `?` is introduced.
Remarkable that people with such little knowledge feel comfortable talking so much.
No, you should not unwrap unless you know it is safe to do so. You should also add a comment why it is safe to unwrap, if it is not obvious.
Many programmers are writing code for sunny weather only, with error handling being something you might add as an afterthought if your code starts to feel a little too brittle.
In my eyes error handling is just as important to do correctly as getting the core of the functionality done, because error handling is a core functionality of any program, especially if we speak of libraries others are meant to use.
Error handling is what differenciates engineering from coding.
What OP meant is that proponents of Rust are often a bit out of touch with reality: Go to github and find a random Rust repo which doesn't use unwrap excessively. And is thus full of serious bugs, according to your wording.
> Go to github and find one Rust repo which doesn't use unwrap excessively.
Consider serde-json, a widely used library to serialise and deserialise json. You asked me to find “one Rust repo”. Ok here it is - https://github.com/serde-rs/json/search?q=unwrap&type=. Of the 22 uses of unwrap, nearly all are in test code or in comments. Of the remaining 3 or 4, they seem safe to me. But maybe they’re not. Could you think of some json input that could trigger a panic from one of those unwraps?
I’ll put my money where my mouth is. I’ll donate $100 to a charity of your choice if you can find that.
But if you can’t, at least have the honesty to admit that you misspoke when you said not even a single repo without “excessive” use of unwraps exists.
Not every use of unwrap is a bug. For example a regex library returns Result on regex construction because the passed regex could be invalid. But if you construct the regex yourself, from a hard coded string, you know it is correct. Then you just use unwrap and it is ok.
The assertion remains true though. Unwrap should only be used if you are prototyping or you are 100% sure it will never actually panic.
It's just like the IndexOutOfBounds exception in Java. Many functions can theoretically throw it, but most libraries and programs do not catch it because usually if it is thrown it means that something happened that the programmer did not expect and therefore the program should crash.
The problem would not be that it is commonly used, the problem would be that it is abused. And I don't see that happening currently.
The assertion that "If a library panics on user data, that's a pretty serious bug" remains true.
If a library is panicking on invalid user data, it is because they are abusing panic, which is a serious bug. Or they just didn't realize that their code could panic, which is also a serious bug.
It panics just like my `my_vector[2]` example does. What did you think `my_vector[2]` did? Libraries use `my_vector[2]` too. I don't get why we're changing topics from one commonly used panicking operation to another.
Unwrap is supposed to be used when the developer know that the error can't happen.
Or that if that error happen there is no recovery anyway, and the best thing to do is to abort the program.
To be a bit fair, checked exceptions in java also have their 'bypass' system, since Errors are not checked. So you can't be sure whether someone will decide to throw an error in the middle of library code. You still have to catch-all. I'm not saying it's better.
I haven't seen a way to do exceptions better than fully-checked exceptions, but you have to be ready to have buffer/integer over/underflow exceptions everywhere or have a fine prover for the absence or runtime erroes to 'allow' you not to have them in your signature.
Otherwise having discriminated records (or option types if you prefer) for return and error-handling seems more down to earth, if a bit painful to write.
Frankly, I love Java's checked and un-checked exceptions differentiation even if the standard library is confused about it.
Make logical exceptions (depending on purpose of interface) into checked-exceptions. Make system exceptions into un-checked exceptions. Document in javadoc with `@throws`
A higher level module can wrap and re-throw into the appropriate exception if needed.
Error handling can be done in the desired place instead of scattered across the code.
Yeah, I also believe that Java is the closest to the best error handling I am aware of. Unfortunately though, it is inheritance based which is a bummer here. It would be perfect with sum types though.
> Everyone says it's not like exceptions, but in fact it is much worse. Panic is stringly typed and you can catch_unwind it
I'm not sure which argument you are trying to make but panics are not stringly typed unless you panic with a string. You can use panic_any(MyPayload) and then it panics with that instead.
Needs a companion blog, "perfectly safe code the rust compiler will nag you about". And the contortions rust programmers go through to avoid that.
Rust is really impressive in a lot of ways. Type classes and pattern are a great fit for systems programming.
But they're fixated on the idea that everything possible should be a static analysis error, language ergonomics or usability be damned. I'd much rather these be warnings, because no static analysis on earth is going to stop you from actually needing tests to see if your code works.
> ... no static analysis on earth is going to stop you from actually needing tests to see if your code works.
Correct. However, the intent of Rust is that the code will not fail [1] due to some silly machine thing below your current level of abstraction.
As an example, I'd point to one thing that Rust does that solves a lot of problems: the Vector. This gives you:
- a buffer to load as needed
- bounded memory usage ( no more than 2x what's actually needed, and the ability to tailor it)
- automatic resizing as needed
IOW, eliminates 'C programmers disease' (eg: #define BUFFERSIZE 1024 // big enough for anything :-) )
I am sick and tired of writing either the tedious resizing stuff by hand, or using a linked list [2] (which in today's world isn't performant: a self-resizing array will deal with locality-of-reference issues much better).
Disclaimer: I've only peripherally used Rust professionally (ie., corporate innovation sessions, self-contained utilities). I have done enough 'fitness exercise' stuff that I'm confident to comment.
[1] "Fail" as in, do something random like start a cryptominer at root-level-privilege. "Fail to compile" is fine, "panic at runtime" is not fine but not worst case.
[2] Opinion: the reason why linked-lists are an example of something hard to do in Rust is because Vectors make them irrelevant, so why bother?
Vectors by themselves don't make linked lists irrelevant, but they remove 90% of the use cases, and most of the remaining ones would be better served by a hashtable, or some sort of fancy tree. Linked lists (especially as C programmers typically implement them) are absolutely awful for performance.
Something I find interesting about Rust is that we can do those safe patterns, as long as we're willing to lose some performance.
The way I think of it: Rust forces us to choose between flexibility and zero-cost memory safety.
If we choose zero-cost memory safety (in other words, we don't use Rc or unsafe or large Cells) we can't do things like dependency injection, basic observers, backreferences, many kinds of custom RAII, etc. But we do get speed.
On the other hand, if we allow e.g. Rc into our codebases, we can do these patterns just fine, though there is a performance hit.
The final challenge in learning Rust (IMO) is to figure out when Rc is better, and when we can afford the complexity cost of zero-cost memory safety. I've seen a lot of Rust projects move mountains to avoid Rc, and ironically end up adding more run-time overhead and complexity.
> On the other hand, if we allow e.g. Rc into our codebases, we can do these patterns just fine, though there is a performance hit.
Interesting... I was using one of the Project Euler problems as an exercise for learning Rust; I found that Rc actually improved performance (guess: by eliminating a lot of copies/moves as things went in and out of scope).
If you try to code Rust like JavaScript by using Rc everywhere, you will quickly run into panics when you try to borrow from a RefCell that a caller has also borrowed from. The limitations of &mut cannot be trivially worked around.
The "trivial" equivalent to JavaScript is not RefCell, but plain old Cell, applied at the level of individual fields you want to write to. No panics and no (relative) overhead there.
RefCell exists more as a bridge back to the world of &mut, since so much of the ecosystem uses that approach. But if you actually have a good reason to write JavaScript-like data structures (e.g. actual interop with a JavaScript-like language) then it just kind of gets in the way.
I don't understand what you have in mind. Here's some basic JS, iterating over elements of a Node:
for (let child of getElementById('parent').children)
func(child)
The equivalent in Rust is "risky" because func might also call getElementById('parent') and then that would panic.
I think you are saying that Cell solves this by temporarily moving all children into a local, iterating over that, and then add them back as children? That seems pretty sketchy to me, you have these weird transient states where Nodes are temporarily removed from the DOM.
No, I'm talking about matching JavaScript's memory layout and memory management, indirection and sharing and all.
The children field is itself a reference to a collection. Reading the field creates a new reference to the same collection, and that is what the for loop holds. If func grabs parent that's fine, it's just one more reference (in the Rc sense).
That collection also just holds references, and the for loop copies them out too. The trickiest bit here is the iteration state itself- you wouldn't be able to use e.g. Rust's typical slice iterator and still retain the ability to mutate the collection, but JavaScript doesn't do that either. Its iterators just have to be implemented taking the possibility of external mutation into account.
The fundamental tradeoff here is that JavaScript simply puts everything behind a reference, all the time, while Rust allows you to work with objects directly. This is what makes sharing+mutation feel so simple in JavaScript- anything you can do to change the "shape" of a data structure is really only changing a reference, and leaving the old version around in case anyone else was still using it.
Thank you, that clarifies the idea for me and it's an interesting approach. It seems like an "all in" design that you might apply to e.g. an interpreter.
Of course in practice we don't abandon Rust's slice iterators to achieve basic observers or backreferences, and I've definitely tripped over "someone else is already borrowing this thing" runtime errors.
Of course, Cell has its own overhead, which is why using large Cells is often worse than just using Rc. It's also very situational; we often have types which don't work with Cell, and it often doesn't make sense to copy objects in many use cases.
It's one of those things that works well in theory, but in practice can fall short.
That's totally beside the point here: you can do a mechanical (thus "trivial") translation from JavaScript-like code to Rc+Cell, without ever hitting that overhead.
That is, JavaScript-like code simply doesn't do large assignments in the first place. It doesn't directly hold move-only objects that are trickier to use with Cell.
If you find yourself hitting those cases, you have left the realm of JavaScript-like code, and entered the realm of manual memory management.
> The limitations of &mut cannot be trivially worked around.
> The "trivial" equivalent to JavaScript is not RefCell, but plain old Cell, applied at the level of individual fields you want to write to. No panics and no (relative) overhead there.
I thought you were implying that we could trivially work around Rust's problems by just using Cell everywhere, my comment was in response to that. It seems now that you were talking about something else, so nevermind =)
And the other extreme, avoiding Rc entirely, railroads one into a certain architecture which is good for only some use cases and not others.
I like to think the community will someday learn when to use Rc. It had better, lest low-overhead languages with more flexibility like Cone [0] overtake Rust.
Do you have any examples of the “perfectly safe code the Rust compiler will nag you about”? Not trying to start language wars, just genuinely curious as someone who writes Rust on occasion.
It's not very good at knowing when something doesn't alias, for example.
E.g. if you do this, the compiler doesn't realize it's safe to do because the array locations are different so I'm not borrowing the specific location mutably more than once. Instead it nags me that I have to split the array into non-overlapping slices.
if (i != j)
swap_items(&mut arr[i], &mut arr[j]);
A contrived example and obviously the same can be achieved in many other ways, most of which the compiler would be happier about - but that's often the case with Rust: a seemingly safe thing isn't quite safe enough for the compiler so you have to do it differently. And that's the main problem of ergonomics in the borrow checker imo.
This is helped enormously by helpful error messages, and there is great progress on fixing little paper cuts and improving the borrow checker to make more valid programs accepted by the borrow checker. But it doesn't contain a massive AI or theorem prover so there will always be situations where you'll need unsafe despite not actually being unsafe, or when you'll do something a bit more contrived than you might have expected.
This is a good example. Another common situation is a struct that has both `foo` and `bar` members and then exposes both `.get_foo_mut()` and `.get_bar_mut()`. (Again this is a trivial example, and maybe in the real world they do more work before returning those references.) The problem is that it's illegal to call either of those while the return value from the other is still alive. Even though we know they don't alias each other, and we could totally accomplish the same thing if the members were public, there's no way for the method signatures to express what parts of the struct they don't touch.
The compiler is seeing the `id` and `f` references as overlapping for the entire arm, even though the use of `id` and `f` are not interleaved. Bearing in mind that I don't actually know how the compiler works here, but I don't think this is a borrow checker limitation in and of itself, rather what I think is happening is that in the match expression the compiler is creating both `id` and `f` directly from `val`, creating the overlapping borrow.
The reason I believe that is that this equivalent code results in the same error[1]:
fn do_foo(val: &mut Outer) {
let f = val.get_inner();
let id = val.get_inner().get_a();
if *id == 3 {
*f = Inner::B(25);
}
}
Whereas if you create the `id` reference from `f` instead of from `val` the compiler accepts it because `f` is not used between `id`s creation and death[2]:
fn do_foo(val: &mut Outer) {
let f = val.get_inner();
let id = f.get_a();
if *id == 3 {
*f = Inner::B(25);
}
}
my go-to example is the non-lexical lifetimes stuff, like when you're somewhere in a match and you don't get to return a reference to a thing because there's another reference that's clearly not relevant anymore at the top of the match
Though it's worth mentioning that Rust gained support for many non-lexical lifetime patterns a few years ago, and has designs on supporting more in the future.
> no static analysis on earth is going to stop you from actually needing tests to see if your code works.
I might have been convinced if mathematical proofs were not expressed in code. If a proof can exhaustively cover the problem space, then there's no need for further testing.
Fair point, but would you deploy un-tested code to production based on a static proof? AFAIK the issue with this stuff is that the kinds of problems proofs are able to practically cover are very small.
Static analysis is an incredibly useful tool, not denying it. I am saying we should avoid worshipping at its altar. Any useful program is incredibly complicated and needs some kind of run time testing.
I don't feel like static analysis and unit testing are very different things. Given that my unit tests run roughly at compile time (ideally they would run during compile), it really isn't different than static analysis. Especially unit tests where all the inputs are mocked and expected outputs are provided.
When I wrote Ruby/Javascript, I remember needing to write unit tests that verified types of input/output variables. These kinda tests were undifferentiated boilerplate that needed to exist but didn't need to be written by me. Especially since there were many tests I'd forget to add or would intentionally not add because the test file was getting obnoxiously large.
Factoring out those boilerplate tests into the compiler (when using Java, Rust, or TypeScript) was very valuable to me, but didn't change the fact that they are basically automatically generated unit tests. The borrow checker in Rust is a similar factoring out of automatically generated unit tests, which I wouldn't have typically written.
Continuing to push more undifferentiated unit testing into the compiler/libs/proofs helps make sure I only need to write domain specific unit tests.
Why do you feel static analysis and compile time unit testing are different? or do you mean, domain agnostic testing is ok, but really we need domain specific testing?
When I wrote Ruby/Javascript, I remember needing to write unit tests that verified types of input/output variables.
When I write in dynamically typed language I never write tests like that. I'm more familiar with static than dynamic, but are experts really doing that kind of thing?
Sure, sometimes I even deploy code without a proof nor tests ;)
Jokes aside, if my problem is small enough to be proved (for whatever value of small), I do not see any more value to be extracted from tests. Who bothers checking that 3+2 = 2+3 after proving that x+y = y+x?
I wish static analysis were always sufficient. Curry-howard doesn't obviate the need for tests. Anything affected by the physical implementation of the computer still needs to be tested, for example. That includes corruption safety for databases, timing side channels in cryptography, all high-integrity code, and so on. Proofs can also be buggy themselves, as can the verifiers.
But yes, formal methods make testing a lot easier and taken far enough, can suffice on their own.
> Anything affected by the physical implementation of the computer still needs to be tested, for example. That includes corruption safety for databases
I think that comes under "taken far enough". If you can model the corruption in your proof, you're good. I'm less confident about timings. But you're right that testing is still useful for bugs on other levels. After all, going high enough, humans can have buggy requirements, and no proof will catch that. Tests might.
I took computer language theory in college literally 20 years ago, had a sandwich with chips during every class. I clicked on that link and could taste the sandwich.
There's a very good chance that in whatever language you're using you're not testing remotely close to what you'd need to in order to reach parity with that of the rust compiler. It's very Dunning-Kruger of a developer to believe that they are achieving such a level of edge case testing.
I hope Rust comes out as the winner in the current crop of possible C/C++ replacements.
It seems like most of the other make a lot of compromises to keep things simple and open.
The bugs are just going to keep happening if we don't do something. And if we don't stop making critical security errors, there could really be a movement to return to analog tech, big enough to set us back 10 years.
So many people don't trust computers, and a lot of emerging tech like self driving cars and IoT in the home relies on trust.
Rust, as mentioned; it was created as a direct replacement to use at Mozilla, among other things.
Zig, which is significantly simpler than C++; good but I'm not sure if it has comparable expressive power (maybe it does, IDK).
D, which is around for some time, but hasn't made inroads comparable to Rust.
Ada, which is around for even longer, and has enough mindshare in certain industries, but sadly not enough generally.
GC-based languages, from Go to Haskell, apparently can't be considered true replacements, even though they are fine for large areas previously dominated by C++.
Nim, if ARC/ORC reference counting is acceptable (which considering I'm using it in embedded firmware right now, it honestly should be, but I can understand why for certain cases it can't be). But it has the same small user-base/lack of inroads as some of the others on your list
Oh for sure. Though manual memory management does mean giving up a _lot_ of the standard library, and ARC is a brilliant in between.
Though you're 100% correct, its flexibility is what makes it so great in my experience.
Honestly one of the nicest things about it for the embedded work I'm doing is knowing that just about everything I'm writing is boringly stack allocated (minus seqs, strings, etc), exactly like we would've done in C ourselves anyway. But when we need to reach for dynamic memory, then ARC has been brilliant. And having RAII-like destructors I've written around C peripheral libraries has been really nice for reducing errors and making it quick to develop in.
D: has been here for a while but never caught up, don't even looks “new” enough.
Nim: quite old also, but gained a bit of momentum in recent years and even reached version 1.0 in 2019, after 11 years of development. Doesn't looks like it gained much more since then.
Zig: unstable, experimental and used to be developed by a single person, recently gained a small community of contributors. May be usable in 5 years but not earlier.
Ada: has important marketshare in some industries, but even in the said industries its future is uncertain because not many people want to work with it. For instance, in one of the biggest train manufacturer in the world, Ada has been replaced by C in some sectors because hiring Ada people was too hard.
Doesn’t Rust place too many limits on what programs you can express to be a viable replacement for C? The premise of C is that you can more or less write any program. Not in the Turing complete sense that is always true, but in the sense that you can make any sequence of memory modifications and control flow decisions and the compiler will produce the corresponding machine code. With Rust, the compiler will only accept a subset of possible and safe programs, the subset that fit in the patterns that the Rust borrow checker can prove are safe.
Rust has unsafe blocks, which let you dereference raw pointers. Also, remember that even in C, if you do something that's Undefined Behavior, the compiler doesn't have to emit instructions that do what you wanted.
If 99% of the time I'm helped by those checks and there is an escape hatch for the remaining 1%, then it's a clear win to have that limit in as many places as possible.
Why would you ever want to wrote something that isn't provable, when you could write something that is?
You always have unsafe blocks for tiny bits of performance critical hardware interaction stuff, but in general, I don't see why we need to be able to access the whole program space in 99% of cases.
Proven by Rust’s borrow checker. That’s not the same as the world of all provably safe programs, which is probably infinitely larger than what Rust will allow.
>I hope Rust comes out as the winner in the current crop of possible C/C++ replacements
What makes you think C and C++ will be replaced?
I see no systems programming language that's simpler than C and no systems programming language as flexible as C++.
Being that C++ is huge already and its developers like to add features after features, it's almost guaranteed that you can find a subset of C++ that suits the way you like to work.
Your comment seems to ignore all the evidence that shows approx, 70% of security related issues come from those memory unsafe languages. Two independent large companies found that same statistic on two entirely different and large code bases. Google and Microsoft.
You can ignore those findings as you like, but that is why so many people are interested in Rust.
All of those are existing software products, the sunk cost into those is so high that overcoming is monumental. Firefox continues its journey. The better question is what should they do with new software.
The analogy I see is an industry similar to the auto industry as it decides what to do with their ICE based platforms as consumers are going after all electric.
People tend to forget that we use those languages, because the gatekeepers of certain platforms only offer them as supported option, with nice tooling and libraries.
Naturally one can bring others into the game, when they are willing to replace the platform owners in the whole stack beyond a bare bones compiler.
I don't see any reason a language needs to be simple, as long as the abstractions are close enough to zero cost. Since Rust can run on even 8 bit microcontrollers, it should be able to replace C in a lot of places.
There might be some set of tools out there that will make C++ as safe as Rust(It seems like some are talking about borrow checkers but finding it near impossible?), but Rust is designed for safety from the ground up.
Plus, even if your linter catches 100% of bugs, your libraries probably won't use those standards, and may be full of bugs.
Not that you'd ever do a JS-level amount of reuse in C, since the lack of standardized package management sometimes makes it not even worth it to use a library, almost like they actively want to discourage reuse.
Rust has a problem with some things being community that should be stdlib, but standard practice in C++ seems to be to reinvent way more wheels than other languages.
People always think the next language, framework, paradigm, etc is going to solve all their problems. It's a pipe dream. You can write garbage in any language.
Some people don’t understand that reducing bugs and eliminating entire classes of bugs is progress. Their “gotcha” is that it’s still possible to write logic bugs, and therefore what’s the point of any language improvements?
Libraries are where unsafe belongs. Having unsafe be an explicit feature is immeasurably better compared to every line in the entire program potentially being unsafe.
Opting out of safety is fine, but safety must be opt-out to be effective, not opt-in. A standard, enforced way to tell readers where the dragons lie is super helpful.
Right, note that I said safe Rust, which is the vast vast majority of Rust out there. This is a major improvement over, say, C, where any line of code could be potentially unsafe.
> And if we don't stop making critical security errors, there could really be a movement to return to analog tech, big enough to set us back 10 years
I'm very curious about the world you live in. Security of IoT is literally the last concern of pretty much everyone I know. The ambient sentiment is "Alexa/Siri/Google is already listening to everything we do anyways".
Privacy isn't anywhere near my list of concerns(At least not for myself personally), but the people who do care, care a LOT.
And even beyond privacy, those people don't like the idea of dependence, not learning paper arithmetic and handwriting, etc.
There are also the rare insane ecofascist types who think technology is unsustainable, and we need to drop it all(And then just let people die until the low tech becomes sustainable...).
A lot of people have a certain amount of respect for Kaczynski in some communities.
Lots of people hate vegetarianism without making any claims at all about the health, they just say it's unnatural, and that's enough reason.
I think there's enough people who really would enjoy a computer-free world to make it a trend to boycott this stuff.
I've always thought that Apple is actually largely an anti-tech company.
While in some cases they do have some excellent high tech under the hood(M1 and their zeroconf network protocols), their aesthetics is almost Bauhaus-level minimal, and the feature set is often randomly reduced(Like the one port laptops).
Their UI is very gestural, involving muscle memory and a feeling of "flowingness" not "proceduralness".
There's a slight sense of "You're smart and capable, you don't need to be a tech person and hide behind a computer, you don't need everything to be perfectly compatible, it's fine if some features aren't perfect" in their restrictive app policies and unique connections.
It's not quite "Star trek dream", where no matter what happens, tech is a point of trust that you expect will adapt to get you out of any jam. They want it to be an appliance or luxury piece of furniture. A multi purpose one, sure, but you're supposed to feel like you'd be just fine without your iPhone.
Sure, everyone has a Siri... but it seems like about half love them, and the other half wish it was easier to just live off grid.
The reason people don't trust "computers" has nothing to do with bugs in the code lmao. It has to do with the fact that they inescapably track their every move and fuck with their minds to completely inscrutable ends.
That shit is basically working as intended, a stricter compiler is not the solution to it.
Does anyone else feel like RAII is poorly named? It seems to me that the largest benefits of RAII that people talk about aren't about resource acquisition or initialization, but releasing the resource. Taking the phrase "Resource Acquisition Is Initialization" at face value just sounds like it describes object constructors.
It comes from C++ where memory being "initialized" to a particular type has a very specific meaning in the language: when a constructor is called on a memory location, it becomes intialized to the type of that constructor, and the reverse when the destructor is called.
"Resource Acquisition Is Initialization". ie. You cannot separate the acquisition of the resource from the initialization of the type (and by implication, you cannot separate the release of the resource from the destruction of the type).
The language already required that initialization/deinitialization is correctly paired for all programs, so RAII was a way to take that guarantee and use it to manage resources as well. And since for local variables it is the compiler itself that upholds the guarantee, the compiler can also ensure resources are acquired/freed correctly.
Without that specific context, the name is poor, but C++ popularized the concept and its name, and once a specific technical name for something exists it's a good idea to continue to use it so as to avoid ambiguity when communicating, even after the originating context is lost.
"RAII" describes what you have to do to make resource releasing work correctly in C++ when there are exceptions all over the place. If you first acquire a resource and then later initialize an object with it, or assign it to an object, there's a time window when the resource has been acquired but is not yet owned by the object, during which an exception will cause it to leak. If, for example, the object's operator= raises an exception before safely storing the resource, you lose.
Alternative names like "constructor acquires, destructor releases" might be better, but I think "RAII" was sort of devised as a slogan to shout at people so they would fix their code.
RAII was invented before C++ considered exceptions.
Back in MS-DOS/Windows 3.x, and Mac OS, CFront/UNIX days, RAII was used for file handles, network connections, other sort of OS handles, and naturally data structures like dynamic strings and arrays.
When exceptions came into scene, RAII was already established as pattern.
I think you're using the term "RAII" in a much looser sense, and we didn't call it that at the time (though I don't have a convenient way to demonstrate this now that Google Groups has nerfed their search). C++ code from that period (and Google C++ code to this day) violates RAII regularly by acquiring resources in non-initialization contexts.
RAII without exceptions is pretty inconvenient: acquiring resources can almost always fail (the only exception I can think of is locks) and so you need some kind of return value, which is precisely what you don't have in a constructor. So you need a flag value in the initialized object which you later have to check, which means that all your resourcy objects are "nullable", in the sense that the type system can't guarantee you that they're in a meaningful state, and you need a dynamic check.
Maybe you mean that people commonly used destructors for cleanup before C++ had exceptions, and I certainly agree with that. But using destructors for cleanup is not the same thing as RAII.
Without RAII, without exceptions, without destructor cleanup, perfectly OK:
int fd = open(fname, O_RDONLY);
if (fd < 0) return FAIL;
...
close(fd);
return OK;
Without RAII, without exceptions, with destructor cleanup, perfectly OK:
int fd = open(fname, O_RDONLY);
if (fd < 0) return FAIL;
File file = fd;
...
return OK;
Without RAII, with exceptions, terrible buggy dangerous code:
int fd = open(fname, O_RDONLY);
if (fd < 0) return FAIL;
File file = fd;
...
return OK;
Note that this is exactly the same as the previous "perfectly OK" example!
You can try to fix this up with catch(...) { close(fd); throw; } but the correctness of such convolutions depends intimately on the precise contours of how exceptions can happen, if at all, in the File constructor.
With RAII, without exceptions, terrible buggy dangerous code:
File file(fname, O_RDONLY);
...
return OK;
Correct version:
File file(fname, O_RDONLY);
if (!file.ok) return FAIL;
...
return OK;
With RAII, with exceptions, perfectly OK:
File file(fname, O_RDONLY);
...
return OK;
Note that this is exactly the same as the "terrible buggy dangerous code" above. The difference is only that now the File constructor signals failure by throwing an exception instead of setting a flag.
This is also the most efficient of all the versions on the happy path because it doesn't waste memory on success flags or conditional jumps (in this function and in its caller) on checking them. It's also the most concise and, arguably, the least error-prone. (Google policy disagrees.)
C++ isn't exactly known for having good acronyms. CRTP ("curiously recursive template pattern") at least tells you that it has to do with templates. SFINAE ("substitution failure is not an error") tells you nothing about the context, nor how it would be used, despite being fundamental to metaprogramming in C++.
> just sounds like it describes object constructors.
I think I've said this verbatim, so you have my vote. I'm curious what other names people might give it.
Wikipedia gives:
> Other names for this idiom include Constructor Acquires, Destructor Releases (CADRe) and one particular style of use is called Scope-based Resource Management (SBRM).
I like your version thought, it emphases both sides directly. You got a resource? Well you have to deal about how to release it too. Maybe just a general term like: Resource Acquisition And Release? Let's be RAAD about being sure we release all resources :)
This is pedantic of me, but I think it matters in terms of what Rust actually provides:
Rust does not provide any resource leak guarantees. The fact that resources tend to be freed when their owning scope closes is a “fallout” consequence of ownership semantics, but Rust itself does not guarantee that dropping an object necessarily closes or disposes any underlying system resources. You can prove this to yourself by writing a thin wrapper over the nix crate’s POSIX APIs: you can leak a file descriptor by forgetting to add a Drop implementation for it.
Similarly, Rust won’t guarantee that all allocated memory is freed. `Box::leak` has well-defined lifetime semantics: it turns a `T` into a `&’static T` by removing its drop handler and leaking the underlying pointer. And this isn’t a problem, because it doesn’t compromise either spatial or temporal memory safety!
It’s true that it’s not a guarantee. But I feel like this is one of those cases where it could be an issue in theory, but pretty much never is in practice.
It's been a while since I wrote Go, but my worry used to be that linters wouldn't be able to follow cases that were just slightly more complicated than average. Like if I open one file, yes, the linter will be able to tell if I don't close it. But if I put a bunch of files in a long-lived map, the linter isn't going to know that any function calling delete() on that map needs to be closing the files too. But in Rust or C++, this case is handled smoothly by destructors. (With maybe a little caveat about whether it would be better to catch any errors that might arise when closing the file.)
It’s nice when you don’t have to worry about what linter you should be using because everyone uses the same static analysis to check for this sort of thing and it lives in the compiler.
And, as we saw in several previous threads: I don't think Clippy lints are an adequate substitute for correctness checks in the Rust compiler itself, plenty of people with more actual impact on Rust agree with me about that. In practice Clippy lints do get "promoted" in this way, though not always as quickly as I'd like.
However, Clippy has lots of style lints which I'm sure some people would be very annoyed to see in the compiler itself. Do you want a Clippy lint telling you that you should do X, and not Y, just because it's considered stylistically better and even though it has literally no impact on the compiled result?
So it makes sense to me that Clippy exists as a linter the problem is the abuse of linters for "Actually programs in this language are dumpster fires unless you obey these lints" and so far Rust is avoiding that.
Sure, I'd like the _ = lock() lint to be an error for example, since you almost certainly didn't mean that, and if you did that's the least useful way to express it.
But time is limited, perhaps I'll get to it at the weekend if nobody else has already done so.
Certainly. Rust has a well-thought-out standard library, and sticking to it will (generally) guarantee that the connection between resource acquisition and memory safety is maintained.
That being said: it can be a problem in practice, particularly in sandboxed or otherwise constrained environments. Leaking a file descriptor isn’t a problem when you have tens of thousands, but it can be one when you’ve constrained the process to just a dozen.
I mean, it's called Box::leak(). It says what it does on the tin. Why was my file descriptor leaked? Oh, I specifically leaked it. As to third party libraries, well, it's a quality of implementation issue. The third party library might forget to flush caches, muddle up file formats, or just crash.
This use of clear naming is one of the things I value in the Rust standard library. Both Rust and C++ provide both a stable sort, and also an unstable sort which may be faster if you know that's suitable. But Rust calls its unstable sort unstable_sort() and so if you don't even know what the difference is you're going to pick "sort" and get no surprises. In C++ "sort" is the unstable sort and if you didn't already know about sort stability then I guess you'll find out the hard way, sucks to be you.
You'll find absolutely no argument from me about these things -- when Rust can't guarantee a particular thing (and there are many, many things it reasonably can't and won't guarantee!), it does an excellent job of its APIs descriptive.
The sole point was one of formal guarantees: this post is about bugs the compiler catches, and the first example is one that Rust will not catch. It won't catch it because, in many cases, it's not a bug at all!
I've definitely run into accidentally reference cycles with `Arc` in concurrent Rust code in production before (due to weak references essentially baton-passing the singular strong reference count to each other before getting dropped. This is a bit of a different kind of memory leak, and even having run into it I'd strongly prefer to write any concurrent code in Rust over any other language I've tried given the choice, but I'd be hesitant to say that Rust guarantees that nothing will leak if only to keep people from getting complacent.
Are you suggesting if rust removed "safe in practice" features (only keeping theoretically safe features) it would lead to less exploitable software? If so I strongly disagree with you. Every language is rife with features that can be used in unsafe way but in practice increases security.
I read this more as a “let’s be precise about what’s actually guaranteed” and not an exhortation to avoid Rust.
Rust is my favorite compiled language, and that’s why I’d like conversations about Rust to be grounded in formal guarantees and not in incidental properties.
I agree it's not something that should be relied upon, nor is it elegant, I'm just pointing out that in the average case, failing to close a file is not a resource leak in the usual sense. It's not like forgetting to close a file descriptor in C.
To answer each of your questions:
1. Generally no, but if your process exits then you definitely aren't leaking a file descriptor. The great finalizer in the linux kernel gets them then.
2. Yes absolutely, but that also isn't a leak. If you're opening a lot of files, you probably have to handle open failing as well anyway.
1. This can lead to programming errors if, e.g., a write is buffered in Golang and Close() flushes the buffer. Then you might not correctly write the file. (I know that if you really cared, you should use fsync, but lost writes could happen in e.g. logging where you don't want the overhead of fsync but you would also like to see all log output, especially on program crash.)
2. I think this is a bigger deal than you are making it out to be. If open fails, how would you handle it without just exiting? I can't see a way of forcing finalizers to run. If you're distributing your Go binary to users, you may not have permissions to increase the allowed number of file descriptors. So your program no longer functions correctly.
Example: A program that processes files in parallel. At any given time it might have 2 * num_cores files open, well below the default descriptor limit on most systems. If I rely on finalizers running, then I might have to exit if the time to process each file is sufficiently short. There is no way to fix this without instructing the user to increase their fd limit. This is bad. Alternatively, if I explicitly closed files, I would never exit.
At the very least, that does not work for rolling back transactions with the database/sql (https://pkg.go.dev/database/sql) package, although it may work for other cases. We've had numerous production bugs result from this.
I’m not a Go programmer, but I assume it’s there for the same reason every other GC’d language has the ability to close resources manually: sometimes you just want to do it earlier (or more explicitly) than the GC would.
Also in Rust, you have combinations of existing and upcoming language features that make certain conceptually simple designs either outright impossible to implement, or only possible to implement a subset of due to certain barely documented compiler quirks, or impossible to determine whether it is actually possible to implement them without sinking hours of time into dead-end prototyping attempts.
Like, not "Rust doesn't do this," but "there's a 50% chance that Rust can do this, but it might turn out to be impossible after all, and absolutely no one can tell which is which, because the language is just barely not expressive enough, or the compiler doesn't analyze this particular case, and might not until both the borrow checker and the type system receive complete rewrites."
And this, in turn, kind of explains why the Rust standard library and especially the Rust compiler are swimming not just in unsafe code blocks, but code that uses features so unstable and private that you can't even enable them with flags on nightly builds.
let wordlist_file = File::open("wordlist.txt")?;
// do something...
// we don't need to close wordlist_file
// it will be closed when the variable goes out of scope
What happens if there is an error when closing the file that I have written to?
The error will be ignored, per the docs for File [0]:
> Files are automatically closed when they go out of scope. Errors detected on closing are ignored by the implementation of Drop. Use the method sync_all if these errors must be manually handled.
If you close your files without waiting for fsync to return first, do you really care if the data has hit the disk? If fsync didn't fail, but close fails, what can you do then? Calling close() doesn't imply anything about flushing buffers or syncing data to disk or anything like that. It's just a signal to the OS that your process is done with this particular resource.
Also, even if close() fails, you can no longer use the file descriptor anyway, since it might be already reused. The only thing you should do with the error from close() is log it to diagnose why it happened and prevent it next time.
I agree it's a bit unfortunate. The rub here would be that `Drop` would become fallible, and if it is fallible, then … how does it fail, exactly? (What happens to the error?)
There's exceptions, but the downsides to such systems are pretty extensively covered.
Nonetheless, the point here is that RAII offers a deterministic close compared to other approaches, at least, even if the write's success isn't covered. You can get that, too, with,
wordlist_file.flush()?;
or
wordlist_file.sync_all()?;
depending on desires.
(And again, I agree that requiring the programmer to remember code in order to obtain safe behavior is not desirable. But this problem manifests in pretty much any other language, and typically in worse manners.)
Yeah, it's not an ideal situation. A lot of the time you just read from the file and then you don't really care about the success of close(), though. Maybe that should be a whole set of different types...
As far as I know, Vale is the only language that can statically ensure we handle that error with its Higher RAII [0], a form of linear typing.
Basically, File's drop() returns a Result, and the compiler enforces that we use it.
I hear linear types might also be coming to Haskell soon, which is pretty exciting. Such a thing is unfortunately impossible in Rust (though many languages can detect it at run-time).
There's just not a language feature to prevent you from letting an object go out of scope without "using" it.
There is `Drop`, which automatically runs when an object goes out of scope, but you can skip that by leaking the object, e.g. with an Rc cycle.
There is also `#[must_use]`, which warns when you don't do anything with an object, but you can explicitly silence that with `let _ = the_object` or `let _bla = the_object`, and it doesn't complain if you exit that scope early (either via normal control flow or panic).
The reason Rust has `Drop` and `#[must_use]` but not linear types (short of "nobody's bothered yet") is that they aren't really all that pleasant to use and they impose a lot of constraints on the APIs you build with them. You would have to prevent leaks of linear objects (which means, among other things, no reference counting or moving between threads!). You would have to track panics in the type system and/or forbid calling most functions while a linear object is live. More detail here: https://gankra.github.io/blah/linear-rust/
The current design means the default way to use a `File` won't handle errors on close, but you can opt into that by making an explicit call yourself. In exchange, you can do a lot of things with `File` objects that you couldn't with a linear `File`. (My suspicion is that Vale either doesn't use linear types for files, doesn't actually enforce linearity at compile time, or makes files painful to use with generic code.)
I've been using higher RAII with Vale for quite a while, and it doesn't have the problems Rust seems to have.
It's actually not difficult to design APIs to properly use handles and other RAII'd objects. Either don't drop generic types (for example, the HashMap doesn't drop any user types), or require a drop generic bound or concept function [0].
If one wants to make it even easier, just take in a "consume" lambda which will take arguments. Then, the caller can specify what to do with these objects. That's how Vale's drop_into function for arrays work, for example.
In my opinion, Rust made the same mistake with `drop` that Java made with `toString` and `hashCode`. We shouldn't needlessly couple functionality with objects, it just causes problems, as is seen with Rust's lack of linear typing. But that's just my opinion ;)
The trouble with Vale's "concept function" and the C++ Concepts it is mimicking is that they're duck typed. We know syntactically that we can do this, but there's no reason to believe it's semantically justified.
This is why you'll notice Rust's traits don't have names like "Iterable" or "MaybeComparable" but "Iterator" and "PartialOrd". They have semantics. Rust's Iterator is not merely any thing which happens to have a next() method but specifically a iterator in which that next() method gets to the next item.
Over in Concepts land they have two "fixes" for this, but they're both pretty unsatisfactory. One fix is, you document what the concept means, and then you just tell the programmer it's their fault when the syntax matches (so it compiles) but the semantics don't (so it's broken). The other is, just tell people not to use Concepts for simple things, if it's complicated enough then likely the syntax will only match when semantically aligned. Unfortunately of course lots of powerful er, concepts, have exactly one match point.
This is funniest when in juxtaposition e.g. Stroustrup first showing off concepts with a simple example like your Fireable concept back in 2018 or so, mixed with Stroustrup in 2020 explaining that it's crucial never to do anything like that because it's too fragile...
> If it helps, think of it this way: a concept function is a 1-method trait on an implicit parameter.
No, that just underscores the problem, it's just duck typing. This "1-method trait on an implicit parameter" doesn't deliver semantics.
Let's look at an actual Rust trait, Eq. https://doc.rust-lang.org/std/cmp/trait.Eq.html and notice that the implementation of this trait is empty. For a duck typing system there is nothing to check, syntactically Eq doesn't do anything.
But of course semantically Eq is rich with meaning. You can't go around using a HashMap with keys that don't exhibit Equivalence, what's that going to do? Nothing good.
I see now what you mean, youre talking about structural interfaces, like Go and Typescript have. Yes, concept functions are those applied to generics.
You may not like structural interfaces, but they're much better at decoupling data from how it's used. People who have used both Typescript and Rust often wish more languages worked with structural interfaces like Typescript does.
It's easy to see why, looking at the examples in the article.
I believe in decoupling, and think a good language will have as little coupling as possible. To each their own though!
Because panics in Rust can force-drop data as part of unwinding the stack. If Rust supported panic-free code easily, it could also add linear data which would eliminate the default Drop and replace it with custom, user-managed operations.
Off topic: what type of error can occur when closing a file? Is it somehow possible that the kernel denies your request, and forces your handle to stay open?
> it is quite possible that errors on a previous write(2) operation are reported only on the final close() ... Failing to check the return value when closing a file may lead to silent loss of data.
> the behavior that occurs on Linux ... the file descriptor is guaranteed to be closed.
So yeah, it's always closed on linux, but POSIX doesn't guarantee that for EINTR specifically, and there are sometimes meaningful errors.
I was referring to checking because "you should check" when you don't actually care enough to structure your program around not losing the data. It's easy to to check and emit a diagnostic, it's harder to make sure you have collected the data you've written to a file descriptor and have something sensible to do with it if the close fails.
In resume, man close(2), gives us the potential errors.
This is the output for Ubuntu 20.04 LTS:
EBADF fd isn't a valid open file descriptor.
EINTR The close() call was interrupted by a signal; see signal(7).
EIO An I/O error occurred.
ENOSPC, EDQUOT
On NFS, these errors are not normally reported against
the first write which exceeds the available storage space,
but instead against a subsequent write(2), fsync(2), or close().
Other comments have mentioned _how_, I figured I'd mention a couple of scenarios that this could create.
Programs that fork/exec will typically close all their open file descriptors, so if a close fails (like an EINTR) it's possible a child could inherit an open file descriptor for a file that they otherwise wouldn't have access to.
File descriptors are also a finite resource so if you're opening and closing thousands of files you could run out.
Both situations are unlikely, but I'm sure somebody has had their day ruined by these.
While Swift looks more polished and much less verbose on the outside, Rust has better 'fundamentals' and community. Swift to Wasm is only an external experiment, not part of core Swift. Pretty much everything outside iOS is only an external experiment, not part of core Swift.
If these issues were fixed, would the Swift ARC compiler be preferable to the explicit Rust semantics, or are there other reason ARC is less favorable than the borrow checker?
There has been effort to be able to use Swift for the backend but it’s not taking off. I like the language for sure. Maybe people prefer to go all in performance by taking Rust?
Android has a lot of Rust now (the bluetooth stack was rewritten in Rust), npm is being rewritten in Rust, Servo is written in Rust, Dropbox' sync engine, Amazon AWS Firecracker, Discord is replacing Go with Rust, Google's new OS Fuchsia has a lot of Rustl. There are also tons of companies looking for hire Rust programmers, e.g. Apple, Intel, Microsoft... there is work to allow for development of drivers in Rust in the Linux kernel.
Compare this to Haskell. Where is it being used? What is it replacing?
Looks like an obvious troll but it's because you didn't take the time to look at job offers. It starts to really pick up. Even here in Paris we start to have several startups that pick it as their main language. And I am not talking about blockchain startups.
The key difference however being that finalization of an object is guaranteed when it falls out of scope, and not at the whims of when the garbage collector decides to run.
It would be a nonsensical idea to leave up to the garbage collector releasing an acquired lock on its own time. Such a situation could very easily cause a deadlock, as a program may be waiting on a lock that the garbage collector must release, but while the program is waiting on the lock, no additional garbage is being generated that might trigger a garbage collector run, nor is there any guarantee that even if that run does complete, that the objects you wanted finalized would be.
You do realize you have settings when you want the garbage collector to do its things, yes? As in what you talk about is "lazy" garbage collector, but there are other degrees of when to run. You can set it to be as aggressive immediately after it detects the object is no longer need it, equivalent of "Object.Free" in other languages.
I think this is a common reaction to <insert thing> being evangelized. I can’t say it’s a good approach to deciding whether or not something is worthwhile or good or w.e, but maybe it’s better this way because it means that when the thing being evangelized doesn’t pan out, not everyone is caught up in the aftermath. That being said, I don’t have anything against Rust or the community, and if it’s a good tool for <job> then it will succeed with or without me or you. Personally I’m interested in how it’s progressing.
Edit: as with many things it’s not right to say that the loudest voices in the room are the community. It could be that this is the behavior you are seeing because somehow it’s managing to get people worked up or engaged in some other way on social media you use, and you find that annoying. If you don’t want to learn the language and engage with the community then you don’t have to, but calling the whole community annoying because people are excited about what they are doing and what they’ve learned and the possibilities they see is… ehhh
1. If the tool is a good fit, why bother with annoying evangelists?
2. I agree, Rust has by far the most (falsely and misleading) advertising about the usage, politics and evangelism going on of all languages around. It annoys me as well. Especially the passive aggressive bullying of Go.
3. Large parts of the Rust community are not acting that way but are friendly and give balanced advice.
4. Focus on the good parts: The language is a good fit for many applications and large parts of the community are great - /r/rust is a good place for example.
I get your point, the community annoys me too, but I was a true believer for a while too and I understand they are just trying to share what made them happy. I root against it sometimes because it feels like its getting so complicated. But mostly I root for it because I have written c++ for a living since before it had an official standard and while the newer standards are better, the compiler refuses to protect me from people who do truly horrific yet subtle things.
The way I see it the things mentioned are nice appetizers and the data race and borrow checker are the main meal.
IME the most frustrating problems are not that you forgot to exhaust the switch statement or didn't initialise a new field, it's when you get a segfault that's hard to reproduce and you have barely a hint about what's caused it.