Removing garbage collection from the Rust language (2013)
pcwalton.github.io
pcwalton.github.io
They are treading a line that is uniquely difficult to tread, and I think it’s mostly working. Async is still a bit of a mess but it seems that’s because it’s inherent to the constraints they had to impose on themselves.
It’s kind of cool that I can use Async stuff in embedded devices[1] and a web app, even if I do get frustrated with mind-numbing Async issues from time to time.
A noticeable portion of async issues come from the fact that a lot of people use Tokio's multithreaded async runtime. Tokio allows you to mix async and native theads, which is both a virtuoso technical accomplishment and also a bit ridiculous.
If you use Tokio's single-threaded runtime, things get simpler.
The remaining async challenges are mostly the usual "Rust tax", turned up to 11. Rust wants you to be painfully aware that memory allocations are expensive and that sharing memory in a complex concurrent system is risky.
In sync Rust, the usual advice is "Don't get too tricky with lifetimes. Use `clone` when you need to."
In async Rust without native threads, the rules are something like:
1. Boxing your futures only costs you a heap allocation, and it vastly simplifies many things.
2. If you want a future to remain around while you do other stuff, have it take ownership of all its parameters.
Where people get in the most trouble is when they say, "I want to mix green threads and OS threads willy-nilly, and I want to go to heroic lengths to never call `malloc`." Rust makes that approach look far too tempting. And worse, it requires you to understand and decide whether you're taking that approach.
But if you remember "Box more futures, own more parameters, and consider using a single-threaded runtime" then async Rust offers some pretty unique features in exchange for a pretty manageable amount of pain.
Also, seriously, more people should consider Kotlin. It has many Rust-like features, but it has a GC. And you don't need to be constantly aware of the tradeoffs between allocation and sharing, if that's not a thing you actually care about.
Like I suppose if I needed a blocking task that was also async, I could send that to a thread pool and then internally spawn a worker async runtime. It's a thinker.
Edit: not block_on, but spawn_blocking. See https://www.reddit.com/r/rust/comments/16ebdi1/comment/jzy7f...
A major issue when dealing with the tokio runtime is Send bounds requirements, I don’t think the single-threaded runtime changes anything because these are API-level issues. You can use spawn_local to avoid migrations but you can do that on the multithreaded runtime just as well.
And then a lot of tools and libraries which get layered over tokio (or assume a tokio environment) will require send futures anyway.
EDIT: Sorry to join in the multiple responses. Clarity for others: "current_thread" by itself does not relax Send, and many libraries aren't configurable to make everything use LocalSet.
Somewhat relatedly, a major (early) stumbling block for me with the multithreaded runtime was trying to keep references across await points. While boxing them is always an option, things also got much easier when I realized that while &T is only Send if T is sync, _&mut T is send if T is send_.
Is it?
> the Send bounds are only there in the multithreaded runtime (because it might need to Send a task across threads).
Tokio is interacted with using free functions which dynamically look up the current runtime, they could not have a different signature even if tokio had different runtime types, which it does not.
> The single threaded runtime will never do that, so its futures don't need to be Send.
Please do point to the ?Send spawn (not spawn_local) which supposedly exists for the current_thread runtime. You can't even spawn_local at the toplevel of the current_thread runtime.
Even in a single threaded runtime spawn requires Send and you need to use LocalSet + spawn_local for !Send futures.
current_thread doesn't even create an implicit LocalSet, if you try to `spawn_local` from the top-level of a current_thread runtime you get a panic, exactly like a multi_thread runtime.
And of course it's an accumulation of trait bounds on everything, which makes for a downgrade in readability.
>Also, seriously, more people should consider Kotlin.
I like Kotlin, but I find its coroutine machinations to be far more confusing that just plain threads (over which Java already had/has some nice quality of life abstractions). And debugging broken Kotlin coroutine code is hell. You will not get a normal-looking stack trace when things go wrong.
I'm not sure you're right about that.
I don't do a lot of low level work, but coming out of Elixir / Erlang I would expect a greenlet system to manage the # of threads based on the hardware its running on. I.e. not single threaded or "you manage the threads too" but "a standard piece of code spreads your greenlet processes between N managed hardware threads where N is determined by hardware + settings." Is that not a thing that the Rust async libraries support?
1. It has a garbage collector, and
2. It relies even more heavily on immutable data than Rust.
This means that the Beam VM can seamlessly move green threads around CPUs and preempt them at arbitrary points, all without breaking code. You never need to know who "owns" something, and you never need to worry about another process mutating it while you're looking at it. The Beam VM is amazing.
Tokio operates under different constraints: There's no garbage collector, ownership can be "lent" to other code, and mutable state exists in a carefully controlled fashion. Despite this, Tokio absolutely supports spreading green threads across all your CPUs and moving them as needed using a work-stealing scheduler (IIRC). But this only works if all your closures are `Send` (safe to move between CPUs). And any closure that outlives the creating code must generally be `'static` (it does not refer to references borrowed from its creator's scope).
Oh, and Rust keeps trying create and manage your async & multithreaded processes without trying to allocate heap memory at all. Unless you explicitly ask it to allocate memory. Which you often should.
It is totally possible to make your closures `Send + 'static`. I maintain several production Rust programs which do that, no problem. But doing so requires understanding a moderate amount of Rust and paying a "cognitive tax" by making a bunch of extra decisions about how to represent things. And I think that cognitive tax is a unwise tradeoff for many problems and teams. But I'm still very happy with my async Rust projects, because they get a lot of value out of fast, memory-efficient async code.
Or C#. C# has async/await (it originated there) and does have a multithreaded event loop, but due to having GC is just a lot easier to work with than Rust+Tokio.
If you're in the "don't colour my function" camp, GoLang is also worth throwing in the mix.
- Language is very, very similar to TypeScript. If you're already doing TS on the backend with Node (or even JS), it's a very small lift to C#
- Very rich standard libraries and first party libraries; reduces the need to import a bunch of third party code
- .NET minimal web APIs are very similar to Express now and perhaps even easier since you don't need to import anything to get a microservice up and running
- .NET AOT with .NET 8 will dramatically improve the cold-start for use cases like serverless functions (I find the cold start already pretty good with .NET 7 on Google Cloud Run with the CPU Boost feature turned on).
- C# has a lot of functional features as a result of F#
- Compiles fast and has hot reload via `dotnet watch`
- Provides access to low level primitives where extra performance is needed> NET AOT with .NET 8 will dramatically improve the cold-start
Don't get your hopes up on AOT. It still has lots of limitations, reflection being a big one. ASP.NET initialization relies heavily on reflection so it will be a while before we can have AOT compiled, HTTP microservices.
> I find the cold start already pretty good with .NET 7 on Google Cloud Run
Thanks for the tip! We're mostly using GKE but have a few services on Cloud Run that might benefit. Do you use the "always allocate CPU" option? We're seeing some memory creep we suspect would be solved by giving the GC cycles when a request isn't in flight.
> ASP.NET initialization relies heavily on reflection
Yes; the first thing I tried when .NET 7 went RTM was switch my web API over to AOT only to have it hang during build..NET 8 AOT is specifically focused on ASP.NET; they've switched over many of the reflection paths to use source generation instead. I have hopes that some time during the .NET 8 lifecycle or .NET 9 horizon, we'll see full support: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/n...
> Do you use the "always allocate CPU" option?
With the CPU Boost option, the cold start is really, really good. Whether you need a warm instance is up to your own threshold for latency on cold starts. I generally avoid using "always allocate CPU" because I'm cheap.The magic of Google Cloud and Cloud Run is really that you don't need to be always on.
- unlike Java, C# supports value types so you can avoid gratuitous overhead or ugly code when all you need is a simple wrapper
It's still painful when you need strict ownership though, except for the limited case of an object allocated directly on the stack.Can you describe what issues you’re referring to? I use the multi-threaded Tokio runtime, I’ve not noticed any overhead with that in terms of development vs. single threaded. Also, multi-threaded async runtimes are generally what you want to make sure you don’t have any single tasks blocking others that could make progress in parallel.
Is there an advantage to multi-threaded async if you're IO-bound? If you want a bunch of concurrent system calls or network requests it seems like single-threaded async can handle that pretty nicely ala Node.js.
Rust was never actually a GC’d language. It had a smart pointer called Gc (and before that @), but that was only ever implemented as refcounting (according to the changelogs some preliminary work had been done towards a precise GC but AFAIK there was no followup).
This is in large part why it was dropped: it was technically redundant with Arc, and could give users the wrong impression, and could always be added back later if that made sense.
If I understand it right, @ is explicitly invoked by the user, but the implementation is embedded within the language. With Arc/Rc, there are Deref/Drop implementations somewhere in the stdlib that do the reference counting.
And while refcounting has lower throughput than more advanced forms of garbage collection, it has a much higher reactivity / lower memory overhead, and it integrates much better with other methods of resource management.
As someone who works with async rust professionally, I wouldn’t let takes like the one you posted dissuade you. My personal opinion is that async rust is easier to work with than in most other languages because of the extra concurrency guarantees that rust gives you. There are some rough edges, but not nearly as many as the link you posted suggests.
Sure Async Rust requires you to learn a few new things, but the blog post is mostly a rant from someone not very familiar with the topic (like what you'd expect from something called “<X> is a bad language” TBH).
That being said, protothreads were implemented using wildly cursed C macros, and offered none of the safety guardrails that Rust has, nor the ergonomics, and had some insane constraints, like "you can't use local variables at all, only statics" because like rust, they were stackless coroutines that meant you would have no control of the stack across await points.
If you were VERY CAREFUL, you could get the same lightweight concurrency with almost the same fundamental model. Woe be on you if you ever had to debug though.
edit - here's a look at how protothreads were expanded: https://dunkels.com/adam/pt/expansion.html
More info: https://vala.dev/
Also, Vale is on the way: https://vale.dev/
And D is also working on it: https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...
So you do not know if there are and what kind and other important details.
>"I think Rust could learn from them."
Yet you think
I think the lesson is that, as with any software, having an open discussion between people who are on the same page about things and have similar incentives (with room for disagreements, as the initial author of Rust had plenty of) is greatly beneficial, without a doubt... but once you start getting people with polar opposite views on things, each backed by a substantial faction behind them, things start getting messy. I'm not saying this has happened in Rust, just that it's not a case the more people, the merrier.
But in terms of overall design it doesn't feel very cohesive, and priority is put in odd places. For example, error handling is an area where people almost always resort to 3rd party libraries. That, to me, having to rely on third party libraries for such basic features is a sign of a serious design flaw. Meanwhile, "clever" things like zero cost map and filter are prioritized and in the language for a long time.
Overall it feels like a language that prioritizes "clever" features at the expense of boring but basic needs. Also any criticism is met with hostility rather than accommodation.
It's a fine language, but I can't say that I love it.
IME people mostly resort to those libraries just to avoid verbosity, since implementing error types is frankly boring and macros/etc make it a non-issue.
Some have neat context-attachment functionality that can be useful as well, though I've personally never needed it.
If you want this function to return an error that encompasses any of the sub-error types you are forced to write the most incredible amount of boilerplate that I've ever seen in a language. You need to define a composite error type and then implement all the required traits for it.
I'm not sure how some people avoid this situation, but it's an incredibly common scenario and the only reasonable solution is to use a library like anyhow. Defining your own errors is also heavily boilerplate prone giving rise to things like thiserror.
Why these third party libraries are not made into first class language features is beyond me, but it is an example of very poor design and is typical of what I mean. Rust implemented a solid error handling core, but then just didn't bother with the things that would make the error handling actually good. Fancy over pragmatic.
I would say a language like Go is well designed in that it has an overall design philosophy and you can see that throughout the language, including in the limitations which are often intentionally chosen. One part of their philosophy that I really appreciate is their intention to make the language pragmatic in industry settings, and making typical things obvious to newcomers is a huge part of that.
Even if your solution is macros (which may be ok, but can also make the underlying generated code more opaque), it should be part of the standard library and part of the language manual. Making things the "officially approved way of handling a problem" has benefits beyond newcomers too, it gives the entire language more consistency resulting in a far more cohesive design.
This is because of what the language is trying to do. Providing map and filter with zero is a priority 0. I think people think Rust had more development manpower than it actually had. Java/C# (and I would bet Go) have had significantly more money poured into them than Rust.
Probably also why Rust has such highly publicized drama, as well. The drama is out in the open, too.
Interesting take, but as someone that drifted away from Rust with the publication of the linked blog post, and wrote their last meaningful code in the language a year later, it does not describe my experience. YMMV, but compared to other languages, I'd say as a first approximation that they had no interest in comments from outside the inner circle.
First, Rust is not actually a memory safe language. It makes writing memory safe code easy and nudges you towards writing "unsafe" code in specific places. This is a good thing and many get away with not requiring programmer asserted safety. But it's also important to note that ultimately it relies on it.
Second, The borrow checker seems to primarily check that you're doing RAII properly. In a sense it answers the question of:
"What if we get the predictable nature of stack allocated memory for the heap?"
Clever, but that leads to a proliferation of lifetime annotations (which are part of your types) and makes code very brittle and rigid if spelled out as is. Because every lifetime that is encoded that way, has to fully cover the scope that encloses all of its usage. And if that weren't enough, it also infects almost every data structure that is some way composed of references.
There seems to be multiple ways of dealing with this issue that I've come across when learning the language:
1. Avoiding pointer references whenever possible and using self-managed vector/slice/hashmap references.
2. Introducing GC via reference counting.
3. Cloning.
4. Macro or trait abstraction.
When we're doing 1, we don't gain much utility from the borrow checker.
When doing 2/3 we would be better served if we used a language with a battle hardened and optimized GC runtime.
I've seen 4 in some cases, but never looked like it's "bringing safety to the masses". The whole API around traits and macros is very rich, very sophisticated and very subtle.
I would rather phrase it as: "Rust brings ML/functional concepts to C++ programmers and it explores a new space of compiler optimizations based on that."
However the Rust _community_ does bring these concepts to the masses. They have written excellent books and recorded multi hour long videos explaining and exploring the language and making this all accessible.
That's an understatement. Rust doesn't require a framework / runtime. Unlike NodeJS, Python, Java, C#, ect; when you ship a Rust program, it has no external requirements.
More important: Because Rust can create libraries that adhere to the C calling convention, you can create libraries that you can call from NodeJS, Python, Java, C#, ect. The fact that those systems have a heavyweight runtime (with GC) makes it very hard to create a library in one environment and call it from the other.
I do think there's room for an "R2" that is semantically identical, but designed to be easier to understand. Probably the biggest room for improvement is using generics for things like RC, ARC, Mutex. I'd rather use something like keywords; and leave generics for user-defined types. This way, when trying to understand what something "is," the "how the memory is tracked" is semantically different from "what the struct is." (Even though under the hood they are the same thing.)
There is an unstable feature for "box syntax" which both allows you to construct a `Box<T>` as let foo = box a (instead of let foo = Box::new(a)) but also to use box as a keyword in pattern matches to deref the box automatically. So struct Foo { value: Box<Bar> };
let Foo { value: box Bar } (in which case value is bound to a &Bar) which was really nice.
It got subsumed into a more generic "deref patterns" project which doesn't seem to be going anywhere but I hope it does because it makes the language much more ergonomic IMO.
Because I want to have a language that I can use to write lean libraries that are available to any language with a C FFI, and that can be linked directly by AOT-compiled languages, without getting into a horrible quagmire of dueling heaps and copying. But I also want to have proper functional and asynchronous programming, and generally just to not have to manually fuss with memory in the higher-level code.
Python and C/C++/Rust/Cython/etc extensions kindasorta achieves this, and it's a huge factor in the language's ascendance in scientific computing applications. Game development has a long history of achieving a similar effect by embedding Lua or Lisp. But I think that it might be more pleasant to have it formalized and baked into a single language.
Wuffs is not a general purpose programming language. It is for writing libraries, not programs. Wuffs code is hermetic and can only compute (e.g. convert "compressed bytes" to "decompressed bytes"). It cannot make any syscalls (e.g. it has no ambient authority to read your files), implying that it cannot allocate or free memory (and is therefore trivially safe against things like memory leaks, use-after-frees and double-frees).
1) Rust's type system or something as expressive 2) C#-level performance 3) GC by default 4) Ecosystem that is as big as Rust
The closest thing seems to be TypeScript (weirdly).
Also, a garbage collector that is state of the art (bump allocation, generational, and compacting) is faster overall typically (throughput-wise, but with less predictable latency) than naïve Rc/Arc all over the place (due to usage of "free list" allocator and counter bumps). That isn't to say blisteringly fast Rust isn't possible to write, and in fact it is pretty easy to write by avoiding Rc/Arc except where necessary and using the stack with the borrow checker, but this entails a writing style that is more "low level" and thus takes a bit more thinking.
1. Interrupting the program to sweep, resulting in unpredictable performance.
2. Possibility that a circularly linked group of variables could be impossible to sweep, resulting in memory leaks.
3. Need to check reference counts (along with bounds checks) that degrade performance.
Lack of GC (and bounds checking) are factors that make C/C++ performant and then lead to the kind of bugs that result in programs that don't do what they're supposed to do (and at worst, result in security vulnerabilities.)
I thought a key goal of Rust was to fix these problems without sacrificing performance.
See my comment regarding "less predictable latency" (although new advances are making for much more predictable latency - see some of the work done in Java GCs for example)
> 2. Possibility that a circularly linked group of variables could be impossible to sweep, resulting in memory leaks.
That is not possible in a precise tracing collector, only in reference counting (Rc/Arc)
> 3. Need to check reference counts (along with bounds checks) that degrade performance.
I think you are referring to reference counting again. Both Rust and C++ have reference counting types as an add on.
State of the art tracing collectors don't collect/check dead objects, they compact live ones, and often don't use reference counting directly. Most objects die young.
> Lack of GC (and bounds checking) are factors that make C/C++ performant and then lead to the kind of bugs that result in programs that don't do what they're supposed to do (and at worst, result in security vulnerabilities.)
Replace "performant" with "predictable performance"
> I thought a key goal of Rust was to fix these problems without sacrificing performance.
Rust, like C/C++, wants you to be able to predict performance and latency and without the baggage of a runtime. Nothing more than trade offs - neither is better or worse. GCs often do perform better with the trade off of latency predictability, but as always it depends on use case as to what is appropriate. Rust likely made the right choice for its domain.
1) this is generally true, but many part of this can be done concurrently, and if we want to improve latency at the cost of some throughput than there are low-lat GCs that maximize the pause times (it is basically independent from the heap size), so in practice you have similar interrupts as you would from the OS alone.
2) this is not a problem with tracing GCs, only with refcounting (most well known is Python, ObjC and Swift for this perhaps). You can still leak memory everywhere by e.g. storing them in a huge list forever, though, but that is a much rarer and easy to debug bug.
3) this is again only true for RC, and it is the reason why it is slower than tracing GCs, especially when it is multithreaded and the increment/decrement has to be an atomic operation.
Whether a variable goes out of scope is trivial in many languages. The problem is with determining whether the lifetime of a value ends.
For example:
func foo(items, moreItems) {
b = new bar()
if coinflip() {
items.append(b)
}
if coinflip() {
moreItems.append(b)
}
// ‘b’ goes out of scope here, but its value
// may live on inside ‘items’ and/or moreItems
// and will have to be destroyed when it’s no
// longer stored in either.
}
In general, once you allow for dynamic allocation and references that can be copied (so that multiple objects ‘know’ of the allocated object), it can be very difficult to determine exactly when the last reference to such an object ceases to exist.Without a GC (i.e. rust), in order to be able to make guarantees about things that it cannot determine at compile time (this is a mathematical impossibility) it restricts the set of programs you can write.
Depending on what you mean by kind: yes. That is, Rust does limit your code’s architecture to a subset that is fine for most things, but not everything.
* Type system is even accidentally Turing complete
* Very good perf, but language doesn't help by being indirection-friendly. Value types will help a lot.
* SOTA GCs
* Ecosystem big
* Cheap threads now. Don't do async! Just block.
* Structured concurrency soonish
A turing complete type system is easier to stumble into than to avoid.
That doesn’t mean the type system is expressive. Turing tarpits are turing complete by definition, and nobody would call Thue, Iota, or the average OISC expressive.
Since, as I feel it, Rust was/is started as an attempt to bring roughly the ML (Hindley-Milner) type system to the area of `systems` (non-garbage collected) development.
In the early days of Rust I thought of it as type inference & algebraic data types meets C++ (now kiss!). But then the borrow checker stuff went in there and it took a different turn.
And I think you'd be surprised by how large the OCaml package community is.
People who work in Rust would likely find OCaml very familiar after a week or two of hacking.
The other option is Swift, but it's pretty ghetto-ized into the Apple ecosystem.
C# is my main language. I consider it a very good all-round language.
Rust's type system has some advantages over C# tho, for example Sum Type, Option (C# has ? but it was added later so you need to be careful when interacting with old code, kinda like TypeScript <-> JavaScript to a lesser extent), exhaustive enum, etc.
Another thing I don't like about C# is the runtime startup time which prevents me from using it for command line tools (Yes I prefer static typed languages even for "scripting"). I think Go has proven that you can have both GC and extremely fast startup time.
JIT:
dotnet publish -c release -o publish -p:PublishSingleFile=true -p:PublishTrimmed=true
AOT: dotnet publish -c release -o publish -p:PublishAot=true
Either one has really good startup time (below ~100ms and 20-30ms respectively depending on what you do), compact binary size and require no external dependencies. Just like in Go except without all shortcomings of Go :)p.s.: AOT on macOS requires .NET 8 preview (will be released in November)
Additionally, https://github.com/c-blake/cligen makes decent CLI tools a real breeze. If you like some of Go's qualities but the language seems too limited, you might like Nim: https://nim-lang.org. I generally find getting good performance much less of a challenge with Nim, but Nim is undeniably less well known with a smaller ecosystem and less corporate backing.
EDIT: I make only observations here, not demands. Another observation on the same machine is `python-3.11.5 </dev/null` with an empty environment taking 7.85 +- 0.02 ms.
EDIT: Also, it's misleading to bundle Nim with C and C's many & storied footguns. While "low-level" is somewhat subjective and you can opt-in to go as low as C (if you so desire), most Nim code is as high-level as Python or C# with various choices in automatic memory management, and the language has very high-level capabilities. E.g., Nim has user-defined operators like Julia. Want to add `=~` or `~` for regex pattern matching? No problemo. In that aspect, Nim is arguably higher-level than C#.
Then the answer is F#. OCaml has everything except the big ecosystem. Haskell has a way bigger Ecosystem than OCaml, but is still not comparable to Rust.
- sum types via sealed classes (not great, admittedly)
- enum "when" expressions are exhaustive
- option type via built-in nullable types (honestly the superior solution)
Pick up some F# then. Both can interop, I've built many things in a combo of the two languages, plus it'll make you a better C# developer and your type system power greatly increases. Be careful though you might not want to go back to C#.
- absence of null pointers
- proper sum types which can be statically checked for exhaustiveness
- statically controlled mutability (yes, borrow-checking is still useful if you have a GC!)
Also no exceptions, but that's not a type system feature.
The compiler will ensure you check for null before de-referencing a nullable type.
In reality, it seems like the python developers and toolchain are embracing rust enough to reduce the benefits to a new alternative.
Sum types are built-in [1] for formal parameters. `nil` is only for `ref|ptr` types. In much code you can just use stack allocated value types and there is neither GC concern nor nil concern, but there is also a mode to help: https://nim-lang.github.io/Nim/manual_experimental_strictnot...
Nim has an easy-ish to use Lisp-like syntax macro system where you just receive & process an AST. So, to do the rest you can make libraries adding the feature without relying upon upstream compiler: such as https://github.com/beef331/sumtypes for variables with sum types or pattern matching libs like https://andreaferretti.github.io/patty/ | https://github.com/alehander92/gara.
Probably the closest would be Kotlin. It has the huge JVM ecosystem, it uses GC, programs run fast, they can be compiled to native binaries using at least two different native compilers (kotlin/native and graalvm). The type system is fairly expressive, albeit not quite as much as TypeScript. It makes up for it with just being a much cleaner and more logical language in general, as it didn't inherit much historical baggage.
There's also a compiler plugin API which is maybe the closest equivalent of macros. It's not really documented or stable in Kotlin 1.x, they're fixing that for Kotlin 2, but there are already a bunch of useful plugins that add various features via compile-time generation and reflection.
1. ADTs with exhaustive pattern matching
2. No exceptions
3. Zero cost generics
4. Traits
5. No nulls
6. No inheritance
As a rule of thumb, languages that loudly claim not to have exceptions actually use them in some way (see POSIX C, Perl, Go).
In idiomatically written Rust you will never obtain an exception/panic (unless there is a bug), and exceptions are not used for control flow. This is not the case in Java or C++ or many other languages.
/// The compiler currently unwinds with a special sentinel value to abort
/// compilation on fatal errors. This function catches that sentinel and turns
/// the panic into a `Result` instead.
That's clearly using unwinding as a control flow primitive.There's another example in src/tools/rustfmt/src/lib.rs, in format_snippet, where panics apparently are just suppressed.
I expect application servers for Rust to handle panics in a similar way: abort a specific request, but not terminate the whole process.
Sun types, affine types, traits.
F# is not magical. Yes it's an ML, but frankly, C# is better in every way, to the point we're slowly moving away from F# entirely.
The main reason is perf, it's really easy to shoot yourself in the foot with performance in c# (e.g. huge allocations, accidentally evaluating seqs twice, etc). Also IDE support for F# sucks when you get to the hundreds of project solutions like we have.
For side projects, sure use F#. For everything else, stick with C#.
1. No null pointers 2. No exceptions
C++/CLI covers some of these aspects, but I haven't used it and don't know how large the vcpkg ecosystem is.
Tons of languages have better type systems.
It would be nice to see if we can have a sub set of C++ that forces us to only use std::make_unique or std::make_shared calls.
Not really. Both Box and Rc/Arc are first and foremost library features implemented using the equivalent of malloc() and free(). Box is a bit special due to its deref semantics, but other than that, there's nothing stopping you from implementing them or something else yourself.
Removing garbage collection from the Rust language - https://news.ycombinator.com/item?id=5811854 - June 2013 (130 comments)
a) A lot of code ends up littered with Box anyways, which frankly isn't any more readable since "Box" doesn't tell you anything until you already know what it is and once you do it's just verbosity.
b) As I understand it, Box gets "special treatment" by the compiler/type system, so pretending it's just a pure standard library component is a bit obfuscatory
c) Heap allocation and heap pointers are first class citizens in other languages, why wouldn't they be so in Rust?
There is a strong desire to stop that and have Box be a normal std type, the main thing that is blocking this at the moment is that Box has special deref magic that is not possible to implement with surface level rust syntax (even with nightly features).
It's still intimidating with lifetimes and Arc<Box<Rc>> like idioms. Still, it's blazingly fast (tm). I wonder what Rust would be like if the GC was kept in like in Go.
It would be one of the litany of managed languages that doesn’t significantly differ in anything from each other, and we would have no reason to be hyped about.
RFC 256, 2014-09. https://rust-lang.github.io/rfcs/0256-remove-refcounting-gc-...
Includes « I (and I think the majority of the Rust core team) still believe that there are use cases that would be well handled by a proper tracing garbage collector. »
https://news.ycombinator.com/item?id=8312327 A core developer says
« I wouldn't be so quick to give up on GC support yet! "We have a plan!" But I don't think we'll have it in for Rust 1.0. And it's true that, even if we never do get it to work in a satisfactory way, the language works just fine without it. »
By 2015-04 ("Fearless Concurrency"), "Memory safety without garbage collection." is a "key value proposition" (this isn't quite the same as saying "we never want Garbage Collection", of course).
2016-08 https://manishearth.github.io/blog/2016/08/18/gc-support-in-... « Recently we (Felix, Niko, and I) have been working on getting compiler-level GC support for Rust. »
2018-10 withoutboats has a research garbage collector as a library: https://boats.gitlab.io/blog/post/shifgrethor-i/ The intro post includes « I do not expect we will ever “add GC to Rust” ».
2021 summary of options: https://manishearth.github.io/blog/2021/04/05/a-tour-of-safe...
They did part one, but not part two...
You're asking for a different language.
Especially with async which complicates lifetimes a lot.
Rust is aimed and focused as a safe C or a sane C++ substitute, and is meant to intermingle with both. It is not an application "high" level language. You can use it as such, which is great. For anything you would use C or C++ you can use Rust instead. As a cryptographer I find that great.
Regardless of all the lack of latency or other control -- beyond fine tuning -- a garbage collected language makes critical memory choices we want to make instead. There are times we want to swap or use our own memory allocator, never mind having to add a garbage collector in the mix. (There is a good number of languages that scratch that itch, and you can likely link and use your C/Rust code with them.)
As far as async: also respectfully disagree. Async is sugar for here is a Future<..>. If you want to poll it locally you can. You can scope it also. If you want to use a cross thread work stealing algorithm you can. But you need like memory management to consciously make these design decisions. This is similarly why a lot of things are not built in in C.
This is part of why Rust is so successful -- it's the first real alternative for this space since C++ came along. For most application development, it's better to use a garbage collected language. But in the application space there is a much bigger choice of languages already available, Rust wouldn't have been a big deal over there.
The interpreted language even be slower than Python, so long as the escape hatch to Rust was simple and safe enough to implement the interfaces.
In Rust, lifetimes color your types, like async colors your functions.
It is a great condensed summary of what makes lifetimes a great difficulty of async Rust. It's a language that has the function coloring that is typically introduced by async (likewise in JavaScript), and on top of that the typesystem itself gets colored by lifetime annotations. You can have a well written and working program... then due to some new need or refactoring, wanting to add a 'static somewhere will cause it all to break down.
It's part of the language, nothing bad with that. But it is an extra layer of difficulty that needs to be mastered. To me it shows that Rust might only be the initial step towards future programming languages where this kind of issue doesn't lean so much on developer knowledge.
https://www.youtube.com/watch?v=p-tb1ZfkwgQ&t=340s
(in case it doesn't work: on the 5:40 mark)
Rust is arguably the first real contender to C++'s reign because it really brings to the table features you'd be a fool to pass on. D was nice, but it was not worth the switch. Eliminating whole classes of bugs instead is.
I actually wouldn't mind a subset of Rust that targets the JVM.
IO complicated. The cycle of doing code then waiting for IO is wasteful (sequentially you waste millions of CPU cycles waiting for the network to answer back). To max out usage of your hardware resources you could just aggregate IO requests with your compiler. Switching back and forth between code that uses IO, as IO request come back (that's async)
Problem is: switching back and forth between code that uses IO is recklessly hard wrt coming up with a proof of the lifetimes of the variables.
And a language/runtime _needs_ to have the trifecta of compiler/GC-or-memory-management/memory-model coherent and within the runtime.
compiler/GC-or-memory-management: the GC-or-memory-management needs to know who writes, who owns, who reads
GC-or-memory-management/memory-model: you need to know when and how you can read your writes, what are the rules
memory-model/compiler: you'll be managing memory barriers so that you can cram together sequences of writes that are compatible, for maximal performance
This trifecta dependence is foundation to a language/runtime, and a change to one affects the others quite deeply. Changing the compiler (bringing async here) affects the other ends and you can't do that when GC-or-memory-management is all over the place (as a lib, or god forbid in user hands)
I'm afraid async is just something unaffordable for a language that wants to be that close to the metal. And even then, async is just a bandaid for a costly threading IO model.
----
Come over to the dark side of Erlang, Go, and Java. We have small threads now. You can just block like there is no tomorrow, and the runtime will have a cheap back and forth. You can just forget about the lifetimes, as the GC will sweep after you (and concurrently, outside of the critical allocation path). Forget it all my friend. Java is love, Java is life.
But such models would also take away everything that makes Rust... Rust.
in the simplest case, you'd add Arc<..> everywhere @ used to be.
Plenty of GC enabled system programming languages have achieved similar feats, with bigger outcome than Rust has managed to on the desktop space, e.g. Xerox Workstations, across Smalltalk, Intelisp-D and Mesa/Cedar.
Redox is still not as feature rich as Mesa/Cedar was on the Dorado in 1981.
https://www.youtube.com/watch?v=z_dt7NG38V4
By the way Go, D, Eiffel, Nim, Common Lisp, Scheme, some JVM implementations are bootstrapped, meta-circular, with their own GC implemented on them.