Bugs that the Rust compiler catches for you
kerkour.com
kerkour.com
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.
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.
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.
Look at the first example. It can be very similar.
Strange example, but I suppose making the corresponding python more complex is one way to make them equivalent.
[1] https://www.starlette.io/ [2] https://fastapi.tiangolo.com/ [3] https://trio.readthedocs.io/en/stable/
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.
Mutating things is mostly not that difficult, just add mut to the declaration and then &mut if you pass it around.
The most difficult thing was lifetimes.
E: borrows got a little tricky as well
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.
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.
My dream is rebuild it: https://tablam.org!
The tools are out there, even Delphi is still being sold and having a yearly conference in Germany.
They declined because only developers working at Fortune 500's get to pay for software.
When one only wants to have free beer, they get to have what the street bazaars can offer, instead of what the monks are using at the cathedral.
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 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.
A few that come to my mind: C++ Builder, QT, Windows Forms, Xamarin, .NET MAUI.
What about Haskell? Can it be similarly high level as Haskell?
Or that the only users you know about are vocal…because how would you know about the non-vocal ones?
Tailrecursive and deeprecursive allow for making any recursive function stack-overflow safe.
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.
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.
GCC started having parts written in C++ 12 years ago: https://lwn.net/Articles/542457/
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.
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.
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 and I disagree ;)
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.
https://frama-c.com/fc-plugins/mthread.html
(And by the way since it is not done by typing it is hard to use on legacy code)
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.
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.
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.
Rust was not stable in 2012 yet.
I do agree with your overall point though.
For all the other scenarios of distributed computing or OS IPC, it does very little.
Plus Rust lacks a story how it can work with OpenMP and OpenACC.
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.
Specially if we take into consideration that focusing back into processes is the only solution to prevent certain exploits.
> 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?
at least the linter will yell at you if you have a panic but not documented it.
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.
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.
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 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.
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.
Are you mistaking Rust for some other language? Error handling in Rust is mostly just using the `?` operator.
The Rust book explains - https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi...
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.
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.
Remarkable that people with such little knowledge feel comfortable talking so much.
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.
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.
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.
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.
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.
* 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.
- 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.
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.
There are always bugs(logic bugs where Rust can't protect you) so why not have a clean interface?
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.
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.
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.
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.
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.
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,
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.
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)
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.
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?
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)
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.
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.
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.
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".
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 :-)
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.
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.
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.
Oh, come on, that's a straw man.
Just because panic! exists as a "abort-program-with-a-message", does not mean it's somehow encouraged above using idiomatic error handling.
Just as you can do the same thing in languages with exceptions. Sometimes exiting the program is the right thing to do.
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.
What if a plugin developer used `System.exit(-1)` in their catch block. How's Intellij going to handle that?
A very popular eclipse plugin did this in past and would bring down the entire IDE when a particular exception happened.
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.
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.
https://doc.rust-lang.org/std/primitive.slice.html#method.ge...
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.
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..
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.
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.
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.
That would allow critical sections that happen to use a library not to need to audit all the code in the library for panics.
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.
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.
https://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspon...
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.
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 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?
That'd be a case for design by contract.
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?
But yes, formal methods make testing a lot easier and taken far enough, can suffice on their own.
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.
Not an example, but if the borrow checker approved every proper program in a reasonable time, then it could solve the halting problem.
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.
enum Inner {
A(i32),
B(i32)
}
enum Outer {
Foo{
field: Inner
}
}
fn do_foo(val: &mut Outer) {
match val {
Outer::Foo{field: f @ Inner::A(id)} if *id == 3 => {
*f = Inner::B(25);
},
_ => {}
}
}
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);
}
}
[0] Playground link: https://play.rust-lang.org/?version=stable&mode=debug&editio...
[1] https://play.rust-lang.org/?version=stable&mode=debug&editio...
[2] https://play.rust-lang.org/?version=stable&mode=debug&editio...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.
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).
But I'm actually curious why the compiler would be making many copies after optimization passes. Release builds or debug builds?
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.
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.
It's one of those things that works well in theory, but in practice can fall short.
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 "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 =)
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.
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.
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.
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?
> I'd much rather these be warnings
Delusional attitude. If it can be caught at compile time it should be; there is no reason to leave it as a warning for programmer to maybe handle.
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.
That shit is basically working as intended, a stricter compiler is not the solution to it.
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++.
I like this flexibility and you can chose the strategy that better fits your needs.
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.
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.
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.
That's why some runtimes have garbage collectors.
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.
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.
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.
https://devblogs.microsoft.com/cppblog/lifetime-profile-upda...
https://llvm.org/devmtg/2019-04/slides/TechTalk-Horvath-Impl...
You can ignore those findings as you like, but that is why so many people are interested in Rust.
Chrome isn't going to adopt any Rust, and Firefox is a few percentilies away to matter at all.
Android now has Rust, yet it isn't exposed to app developers, NDK is all about C and C++, as is the newly introduced AGK for game developers.
AMD/Intel/NVidia now employ many of the ISO C++ contributors and are pushing on using standard C++ as the lingua franca of GPGPU computing.
Azure Sphere, being sold as top security IoT device, has C SDK.
The new WinUI / WinAppSDK are mostly coded in C++, with bindings for other languages.
Apple just released C++ bindings for Metal, as alternative to the existing Swift ones.
So, yeah their business units don't seem to be very keen in following the security advices anyway.
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.
Regular consumers are going electric when they become cheaper and usable as ICE vehicles, until then they are fine for rich people with garage.
> I see no systems programming language that's simpler than C
I guess that depends on what "simple" means
Mostly that's for reasons unrelated to the class of bugs the Rust compiler prevents.
I wouldn't trust Google or Facebook more if they were using Rust. :)
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.
If removing a large number of possible programs reduces bugs without limiting features, that seems like a win.
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".
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.
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.
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).
"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.
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.
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.
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.)
Me, me, me
It should be RAIR - Resource Acquisition Is Release.
P.S. Never mind. As somebody else mentioned SBRM - Scope Based Resource Management -s the best I think.
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!
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.
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.
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!
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.
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.
People can't even agree what unsafe is suposed to be used for, as certain groups misuse it for dependent types programing.
But time is limited, perhaps I'll get to it at the weekend if nobody else has already done so.
> // defer resp.Body.Close() // DON'T forget this line
This doesn't actually leak the file / connection / etc in go for most situations.
When the object gets GC'd, the finalizer runs (https://cs.opensource.google/go/go/+/master:src/os/file_unix...). That closes it.
Both of these are desirable properties.
The finalizer is there to prevent a subset of resource leaks, not to be relied upon.
1. Will finalizers run on program crash?
2. Will I run out of resources if the garbage collection doesn't run for sufficiently long time? (E.g. # of open file descriptors.)
3. Does the order of finalization matter?
Finalizers in Java were deprecated for the above reasons (and more). https://stackoverflow.com/a/56454348
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.
3. No order is guaranteed
Most of this is documented on https://pkg.go.dev/runtime#SetFinalizer
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.
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.
[0] https://github.com/rust-lang/rust/pull/96709#issuecomment-11... [1] https://sabrinajewson.org/blog/the-better-alternative-to-lif...
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?> 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.
[0] https://doc.rust-lang.org/stable/std/fs/struct.File.html
Sounds like a source of bugs any time your file system isn’t 100% reliable.
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.)
> 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.
Checking, on the other hand, probably makes it loud loss of data.
Here is a better explanation than I could write: https://www.joeshaw.org/dont-defer-close-on-writable-files
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().If you use NFS or other specific drivers you can probably get more interesting errors.
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.
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 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.)
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 ;)
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...
C++ uses duck typing, but Vale doesn't have to.
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.
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!
The best external reasoning I could find is this
https://medium.com/@eonil/choosing-swift-vs-rust-237bcb45d97...
So here are my own guesses:
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?
Compare this to Haskell. Where is it being used? What is it replacing?
// we don't need to close wordlist_file
// it will be closed when the variable goes out of scope
AND// mutex is released here as _guard is dropped
That's a fancy name for good old garbage collector from Java
https://docs.oracle.com/javase/tutorial/essential/exceptions...
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.
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
A great way to reduce the annoyance of evangelists is to ignore them, not conjure them up in a place you admit they are not even ruining.
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.