While Rust has mechanisms to deal with this (copying, Rc<T>, etc.), all these mechanisms add pain points, and for problems that are not just theoretical. It affects a number of design patterns, closures that survive the scope they were created in, functional-style programming, functional data structures, any data structure that naturally involves a DAG or cycles.
The difference compared to a (good) static type system is that a good [1] static type system does not measurably reduce expressiveness; Rust's ownership model does.
This is not to say that there isn't a point to Rust's model. Rust directly competes with garbage collection as the main alternative model to achieve memory safety and is an obvious choice in cases where garbage collection is not a realistic option. But where garbage collection is an option, the tradeoffs that Rust makes become much less attractive.
[1] Obviously, primitive static type systems (example: Java up to version 1.4) do pose a problem.
Second, this article only considers the costs of a stricter borrow checker, not the benefits. It will be more work for the coder to have to think about how allocation and ownership is communicates. But I've been through much bigger headaches in production code at inconvenient times (for the business plan) due to dangling pointers, multi-writer concurrency bugs, and ill-defined memory models in software designs.
Even if Rust is much harder than alternatives instead of just different, and even if that's inherent in borrow checking, it's not clear that it's still not worth it in the long run.
One obvious argument is that Rust frontloads its difficulty. It's a terrible tool for most 30 line tasks, simply because the majority of your work will go into arguing with the compiler. But that's true of a great many tools - if there's an adequate payoff in safety and comprehensibility at scale, they can still be worthwhile.
That doesn't solve the learning-and-usage problem, though. Most of us are aware that the explosion of dangerous-at-scale tools like Javascript is largely because no one adopts a language that's unfriendly until you reach enterprise sizes. Even if Rust is "worth it", it's unlikely to catch on while the learning experience feels like pulling teeth.
This arguing with the compiler is really only while you are learning Rust and it's nuances. For me the first two weeks were this way, then I started thinking more in Rust terms. Now I rarely deal with the borrow checker at all, but I do end up in some complex type situations.
You're spot on with the frontloads difficulty. Honestly though, I can't think of any language that I've learned that in under a few weeks I knew how to properly do things in the language.
Python was this way for me. My code looked like C when I started with it, and then I learned after a few weeks about 'with' and 'yield' and started writing real code.
Rust is worth the initial investment, but the first two weeks will require some perseverance.
I definitely share your concern; for me, Rust was like putting on a glove perfectly shaped for my hand. It works and prevents every basic pattern that it took 10 years to learn and start applying to all my code thereafter. If I had Rust all those years ago, it would have saved me a ton of time... but I probably would have turned my nose up at it, b/c I wouldn't have had the experience to understand what it was saving me from.
So yes, I want the learning curve to be lower, but not at the expense of losing all of the safety it brings to developing fast and reliable software.
That's an example of just two blocks, but if it's deeper structures then ignoring this until the end could cause you to go back and restructure significant sections of code to make it abide by the mutability rules.
The process I use to stay productive, is to write small blocks of code, write a unit test for it, compile, and then move on. That way I don't build up a ton of compiler debt...
It just means you've learned to head off problems before they occur, that does not mean the problems you're heading off should be there.
It does. Ada did too with people saying same stuff. Once they got used to it, they swore by Ada since their stuff usually worked after they got it past compiler. I also add that the alternative to Rust for GC-free safety of heap code is essentially separation (VCC) or matching logic (KCC). That's probably much harder for average programmer to work with than structuring stuff to get through borrow checker easily. Cyclone and then Rust were huge steps up on dynamic, memory safety given they're accessible to people who don't know formal verification.
That's the comparison people keep leaving off griping about borrow checker. I think Rust looks a lot better when one adds it. Still a headache but good in relative way.
No, unsafe would be overkill and if you needed unsafe, it would put Rust at a massive disadvantage vis-à-vis garbage-collected languages.
Simply put, Rust doesn't handle multiple ownership well. It results in syntactic bloat and/or runtime overhead. But multiple ownership occurs naturally in a number of real-world programs.
> Second, this article only considers the costs of a stricter borrow checker, not the benefits.
This is why I very specifically mentioned that it's a tradeoff.
Destroying a circular list takes some thought to make sure you don't double free, same as Rust. Rust just makes those sections of code very explicit and calls your attention to it, while in C you may end up with a segfault, or worse a program that only shows odd bugs during runtime. At least Rust is telling you that you need to put some extra thought in this section.
If you don't need manual memory management, it's obviously going to be a burden. There are still use cases for manual memory management though, and that's where Rust comes in.
Use the right tool for the job.
That's exactly the point that I made in my original comment?
I don't see an issue with 'unsafe' if it's an implementation detail and well-tested. Obviously it would be better to avoid it if possible. 'unsafe' is overkill sometimes and worth it other times.
unsafe should be IMO, reserved for only those cases where strictly needed, e.g. FFI is the clearest example.
The same can be said of C and C++, at which point, why does it matter if you use Rust?
The only thing you cannot exactly prevent is "cast_dammit_cast".
All of these things have a larger cost than Rust: you're forced to use pervasive reference counting and very defensive data structures. Rust allows you to get away with pointers deep into other things, as well as aggressive data structures, all in safe code.
Rust also has reliable use-after-move defense for arbitrary resources (not just vague dynamic protections for only the memory resource), and defends against data races. That is, Rust has true safety in a way that can give higher performance than approximate safety in C++.
Static analysers and code reviews are not always possible, depending on the target OS and compiler being used, or how the whole project across teams is organized.
It certainly takes a lot more typing to get things done.
It's much like visiting other, radically different countries than the ones you've been brought up in. Even if you don't wind up moving there permanently, you'll be richer for the experience.
This is pseudo-jargon. What counts as a "measurable" reduction? Do you have any substantive evidence that "good" typesystems don't reduce it? And why are only type systems which satisfy this property "good"? You talk cogently about tradeoffs later on, but this sentence taken literally suggests only a type system which doesn't make this particular tradeoff can be "good". Why?
My go-to languages for the past few years have been Haskell and Racket. There have definitely been times I picked Racket because I knew Haskell's type system would interfere with my expressiveness. Certainly Haskell is less restrictive than Rust with regards to ownership, but all type systems restrict you. I think GP is spot-on in pointing out that you just get used to it over time as you learn to think and code in ways that avoid pushing against the type-system's weak spots.
> But where garbage collection is an option, the tradeoffs that Rust makes become much less attractive.
Yes, precisely. It's not a fundamentally different issue from getting used to other static type-systems; it's just a different tradeoff.
Actually, not jargon. I had Matthias Felleisen's characterization of expressiveness [1] in mind (incidentally, he's also one of Racket's authors).
> Do you have any substantive evidence that "good" typesystems don't reduce it?
If we want to be precise, there is a continuum from statically to dynamically typed language. A dynamically typed language is, after all, the same as a static language with just a single polymorphic type. This means that we can trivially translate a dynamically typed program into a statically typed program, though obviously we don't gain anything by that at this point (this is essentially an argument that Bob Harper has advanced before) and we pay for it in additional verbosity. However, in practice we know that we don't need all that flexibility and can constrain types more, allowing us to get better type guarantees; static types then also allow us to do things that we can't do in a dynamic language (such as compile-time overloading).
[1] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.4...
> This means that we can trivially translate a dynamically typed program into a statically typed program, though obviously we don't gain anything by that at this point (this is essentially an argument that Bob Harper has advanced before) and we pay for it in additional verbosity.
This is exactly the opposite of the argument I advance below; you're claiming that there is a "local" translation from dynamic to static. And on reflection, you might be right, but I'm not entirely sure. It hinges on exactly what counts as "local". Translating away exceptions, for example, requires inserting case-analyses which propagate an exception if one has been thrown literally everywhere in your program. It's "global" in the sense that it touches every node in the program; it's "local" in the sense that the transformations performed don't involve any global knowledge or analysis. I'm rereading Felleisen's paper now to see how precisely he defines things.
ORIGINAL:
> I had Matthias Felleisen's characterization of expressiveness [1] in mind (incidentally, he's also one of Racket's authors).
Great! I believe, by that definition, your assertion that "good" typesystems don't reduce expressiveness is false, at least for common type systems such as Haskell's, Java's, ML's, etc. There are plenty of programs which require non-local rewriting ("a global reorganization of the entire program") in order to appease the typechecker.
A simple example can be adapted from the expression problem: suppose I'm writing an compiler for a simple language, so I have an AST datatype and some functions on it. Then I add a pass to my compiler that removes a certain feature from the language I'm implementing - a certain branch of the AST. Now I wish to simplify my backend by removing the case which handles this branch of the AST. I cannot do that without either dynamically failing in that case (which a type system might even prevent me from doing) or introducing an entirely new AST type which does not have that branch - but this could require duplicating or else considerably generalizing existing code that operates on the AST (a global reorganization). Of course, a sufficiently powerful typesystem might let me do that, but for any typesystem you name, I suspect a similar example can be procured.
To get more precise, the "feature" we're considering adding is "ignoring the typechecker and running anyway". For example, GHC's -fdefer-type-errors. The question is "can any legitimate program written with -fdefer-type-errors be locally translated into an equivalent program that typechecks without -fdefer-type-errors". Without reaching for an explicit escape hatch like unsafeCoerce (which is analogous to "unsafe" blocks in Rust), I think the answer is no.
That said, with regard to your "kill a branch in your AST" example, you can kill a branch with Void:
import Data.Void
frobnicate :: Either a Void -> a
frobnicate (Right x) = absurd x
frobnicate (Left x) = x
Now you can rely on no-one accidentally handing you a Right expecting you to do something reasonable with it.Haskell is not very good at extensible runtime polymorphism (one of its weaknesses). Try OCaml's polymorphic variants.
The point here is that the type system does not restrict me from implementing a data structure that adheres to this constraint, even if it does not enforce it for me.
(Again, not arguing against your thesis here.)
I also agree about your other points, I think maybe one of these days the borrower will become more sophisticated, but these issues he's complaining about are the exact reason I put Rust down. It just felt too sophomoric, as if the language wasn't really ready.
Not all is bad, I really like the way errors are handled in Rust, but there are definitely some problems with the approach they take.
I don't know. I picked up OCaml fairly quickly, but I have consistently struggled with Rust, trying it for a while before abandoning it in frustration with lifetimes, 2 or 3 times. I can program in Rust, but it still seems like an ongoing struggle, as opposed to the smooth brain to text programming I've gotten used to in Python, JavaScript, OCaml, and Lua, and to a lesser extend C, Go and Elixir (just because I haven't used to latter 3 as much, at least recently).
I think Rust may be ideal for embedded software development, or low-level systems software, but for general application development, I think OCaml or perhaps Swift or Scala are more ideal, at least for me (or at least until I decide to try Rust again, perhaps it will stick this time).
Edit: Oh yeah, and I was even able to become productive enough in Scala to build a significant project (a programming language) in a relatively short period of time. Now, I'm not silly enough to think I've really mastered Scala in such a short period of time, but I was using it productively as a functional language as I would OCaml with little difficulty. On the other hand, I've started 3 (3!) programming languages in Rust but have yet to get beyond much beyond the parser/AST stage before I quit in frustration.
This isn't a good thing, in my opinion. Brains make mistakes. I don't want smooth, uninterrupted coding. I want the compiler to tell me when I'm making the mistake, even if it makes things choppier. It's a lot less painful to write code in fits and starts than to write a million tests or spend hours debugging.
E.G: one of my friend has a streaming website. He make banks with ads despite the site having problems all the time. He doesn't care the least. He is not an expert coder and just want to be able to build the features he needs, even if it's not perfect. Types are way too much for him. It took 2 years to master git basics.
Another example: my current clients are geomaticians. They code in Python a lot. They produce only small codes, and don't want something complex. They are experts in maps, not code, and love Python to have the balance of cost / reward they want.
HN is a bubble of expert devs, but the world is full of PHP plumbers, data manglers, sys admin and other professional that have a legitimate needs for "smooth brain to text programming".
Even in Rust's ideal future, very little of the code atop the stack is written in it. It's the code at the very bottom that underpins everything whose share it hopes to eat.
As I often say on HN, I must be missing the sarcasm here...
You don't think the OCaml type system would do this? OCaml gives you just as much safety as Rust does, it just doesn't also give you no GC like Rust does. And the price Rust pays for the absence of GC is the lifetime concept, which adds a very significant layer of complexity to the language. Much like OCaml, Swift and Scala also provide very safe type systems.
Again, if you can't have a GC, then Rust is an excellent option, and the only really safe GC-less language unless you use C with FramaC or similar static analysis (and FramaC also requires lots of training and time to use, just like Rust).
Upcoming OCaml multicore solves the shared state and data races problem with algebraic effects[0]. It is working, it's just not yet been released in mainline OCaml. Supposedly, that will happen this year, but we'll see.
0. http://kcsrk.info/ocaml/multicore/2015/05/20/effects-multico...
Which isn't to say OCaml is trivial, or Rust is useless (GC and threading both being limitations). It just makes me skeptical of claims that Rust's difficulty is irreducible.
OCaml is actually used for many systems programming tasks that can live with a tracing GC.
But you can run OCaml on a microcontroller:
Upcoming multicore OCaml will provide such guarantees through algebraic effects [0].
0. http://kcsrk.info/ocaml/multicore/2015/05/20/effects-multico...
Second, Rust also prevents legitimate sharing in a bunch of cases where concurrency is not involved. (and a simple scoped allocator succeeds)
This would be fixed if there was an easy way to have typed memory that can be shared but cannot be accessed concurrently. There is none. You get at most full unsafe.
You can write arenas in Rust.
You cannot implement many data structures in a decent performance way without unsafe, so Rust guarantee is weak.
It is not extreme, and it isn't even necessary: just reuse one of the many arenas others have already written, e.g. https://crates.io/crates/typed-arena .
Copy-on-write is fairly ease to write in Rust, e.g. the even core Rc and Arc types have a get_mut function that does copy-on-write.
> You cannot implement many data structures in a decent performance way without unsafe, so Rust guarantee is weak.
You can't implement anything in C or C++ without unsafe: Rust's main strength is the ability to write high performance code with a safe interface, so that only one library has to muddle through the `unsafe` problems (if they have to touch `unsafe` at all). Most code one writes is not implementing data structures.
As a demonstration of this strength, even very low-level projects like operating systems can get away with not using `unsafe` everywhere by packaging it up into safe interfaces, e.g. http://os.phil-opp.com/modifying-page-tables.html
If you're not aware of it, scala-native looks very interesting... It looks to be active project.
https://github.com/scala-native/scala-native
FWIW, I've also enjoyed Swift development quite a bit. I think Swift will go deep into the C++ sweet spot.
However Rust is already picking up steam in Firefox modules, hobby OS development, university CS degrees and GNOME modules, so there might be a path there.
I think Swift could play a decent role here if it ever gets good android support as it already enjoys first-class support on iOS. I also agree in that Swift feels very nice. :)
Coming from C/C++, I know that there are structs in memory and can visualize what they look like - surely I can just go and tweak them? (No I can't because either it wasn't safe to do that anyway, or I need to explain to the Rust compiler why it is safe, and that can be quite difficult.)
Coming from something like Java, I (probably) don't think too much about what the underlying structure in memory is, so I'm less likely to try to do this.
Either way, it feels like part of the learning curve is learning the "Rustic" way to do something - in general, you probably shouldn't be reaching for "unsafe" until you know what you're doing! ;)
I tend to agree with this sentiment. The moment I saw that OP was doing pointer arithmetic: I knew they were in for a bad time. It's not that you can't do it -- it's moreso that Rust's raison d'etre is to highlight the flaws inherent in that approach.
I hadn't thought about it, but I think you're right, my background in managed languages is probably why I didn't run into these sorts of issues until after I was already comfortable with Rust. I wasn't trying to make maximally storage-efficient collections right out of the gate, instead I was building application-level stuff on top of `std::collections`.
Which is what's being discussed.
To me it's not a problem at all, Rust is working exactly as designed. When you start writing code in the style that Ayende has: its memory layout becomes entirely non-obvious. The structure definition no longer lines up with how that memory is actually being used. You'll need documentation of the sentinels & invariants, you'll want diagrams of the memory layout (since it won't line up with the structure definition), and a careful understanding of every line of code packing the bytes -- that's just to get a basic understanding of what's actually going on.
This is fine, and you absolutely need to be able to do it, which is exactly why Rust provides `unsafe {}` (along with `std::ptr` and `std::mem`.) Sometimes you need that control you want a memory-efficient data structure, other times that's because you're reading registers directly off the hardware. In either case Rust is going to make you use `unsafe {}` which is a signal to say: "to understand how this is really laid out in memory you need to understand every single line in this block."
I would much rather be able to grep for `unsafe {}` and audit parts of a trie-map for memory safety, as opposed to auditing my entire program for memory safety violations. That's not a failing of Rust, it's functioning exactly as intended. Trying to squeeze every last byte out of a struct is neither obvious nor safe, it requires careful thought.
you mean auditing the entire implementation of the trie data structure, not the entire program.
Guess what?
In rust, if you use unsafe, you also have to audit the implementation of the entire trie datastructure.
And if you think you don't, then I submit you don't understand Rust safety that well.
The code in the blog post looks very much like C-translated-to-rust. Right now, systems programming mostly does look like C/C++ because those are the only two mainstream languages that work here (D, too, but D is like C++. There are also other languages). But systems programming doesn't have to look the same or use the same primitives.
Do you really need to use malloc to allocate memory in a systems language? Do you really need to use pointer arithmetic to deal with things in a systems language? Sometimes, but not necessarily often. Even the Rust operating systems have managed to minimize the amount of unsafe/C-like code.
Rust does expose this functionality, but if it's not something it expects you to use often it makes sense that it might not always be straightforward to use.
To add to this point, my answer would be a big NO, if one would look to the C alternatives that sadly became niches.
Sadly the adoption of C has lead to younger generations thinking that those are the only valid paths in system languages.
Thankfully new languages, including Rust, are changing that.
It is written from "I have one CPU" side. Even C is far beyond that with things like OpenCL...
In any case, Rust is definitely not written with "I have one CPU" in mind. A slogan that is sometimes used for Rust is "safe. fast. concurrent.", and it's one of the headline features on the front page: http://rust-lang.org/ . Rust's abstractions allows writing extremely high performance libraries like rayon, which, had better performance than even highly optimised C libraries like Cilk, in one benchmark I heard about. Building out that parallelism ecosystem takes time, so it does not seem particularly surprising that there are more (unsafe!) libraries for the 40-year-old language, than (safe!) libraries in the less-than-2-year-old one.
You get to use unsafe almost exclusively in the internals of such access where it shouldn't be necessary if there are ways to specify more properties than just binary "safe" and "unsafe".
It is extremely hard to prove such code correct in Rust.
The only way I can make this make sense, is having cross-platform abstraction around atomic orderings etc. (like the new C++11 std::atomics) that works on x86 and ARM and PowerPC etc.
Rust has such a cross-platform abstraction, almost identical to C++11: https://doc.rust-lang.org/std/sync/atomic/index.html
Please be more specific about what exactly you mean if I am wrong.
If you're adding performance considerations I think the answer is going to be yes.
polymorphism is of course an abstraction, the idea of cost-free is that you only pay for the abstractions you actually use.
This is not the same as maximizing performance.
The real question is whether or not you can do these things without giving the developer the control to maximize performance, and I think the answer is going to be no. The response will be that it's "good enough", but that's not actionable as we don't know what "good enough" means for any particular project.
You can build (or use, from the stdlib) safe abstractions over malloc that have the same performance. This should cover 99% of the use cases of malloc. For the few quirky cases where you still need malloc, you can still use it, but that means that the language doesn't need to make malloc-based memory management easy.
You can't do systems programming without them, what you can do is minimize their use by building abstractions, which other languages, such as C++ and D, already do.
So this idea that people somehow think you have to use the actual malloc call throughout your code is a bit of a strawman.
So yeah, (modern) C++ also contests this claim too. Rust contests a stronger claim about safety as well.
------
You're the one who said that "it's claimed to be a systems level language, and if it's really that painful to do systems level things," -- my point is that in both Rust and C++, "systems level things" don't need to involve pointer arithmetic and other similar things. They should be niche tools you reach for when the abstractions fail you, and the abstractions should not fail you that often. This is already true for both languages, I feel. It's not "niche" in C++ because the ecosystem needs time to evolve to that point, but for new code it probably works out.
Then there's the C constructor convention through opaque void pointers idiom.
This stuff is not new, not even close to new, NO ONE, not even the kernel developers, would make the argument that you cannot build abstractions around malloc.
If that's really what you're arguing, well you're not saying anything that C developers don't already practice.
smart pointers have been around for a while too. I see some codebases using them. But many don't.
> NO ONE, not even the kernel developers, would make the argument that you cannot build abstractions around malloc.
I never said that kernel devs or whatever would make that argument.
----------------
Look, you seem to think I've said something different than what I did here.
You said "if it's really that painful to do systems level things, then there's a problem somewhere.". I'm saying that Rust contests the claim that malloc/new/whatever must be easy to use for systems level things. Easy-to-use abstractions around them work well too. It's not that C++ doesn't have these abstractions, it does, and has for a while. My statement isn't one against C++.
My point is that the difficulty of using programming paradigms designed around malloc/new should not be a dealbreaker for a systems language, provided that the same tasks can be accomplished without raw allocation using a suitably generic abstraction. C certainly promotes malloc-based-paradigms. C++ has good alternatives, but a lot of the lower-level stuff I find avoids them because ultimately malloc/new aren't hard in C++. Rust makes them harder to use, but provides sufficient abstractions/abstraction-building-power that this should not be an impedance to systems programming. And, again, I'm not saying C++ doesn't -- I'm just saying that C++ also makes it easy to use malloc/new-based patterns, so you will continue to see those in systems programs even if they aren't necessary.
Now, interestingly, an abstraction for a vec-with-header doesn't actually seem to exist in the crate ecosystem, probably because folks don't like unconditional allocation. But it's relatively easy to build, might do it over the weekend.
(You can fake it in safe code using DSTs but it's not so great)
You may not like that they tend to be very thin abstractions, but they are abstractions, and the point is that the idea of building abstractions around malloc isn't new and certainly isn't unique.
You said:
>> You can build (or use, from the stdlib) safe abstractions over malloc that have the same performance.
While in defense of the statement:
>> Rust contests the claim that you actually need to do pointer arithmetic and stuff often in systems languages. You need to do it a bit to implement your abstractions, maybe, but it is not the first tool you should need to reach for.
This is presented as something unique to rust, which isn't even close to the accurate. This is why I called it a strawman. No one, not even kernel developers using C, are incapable of building abstractions around malloc to make it easier/safer to use.
The rest of what you say is inconsequential in my opinion. This isn't about C++, this is about your implication that building abstractions around malloc is something new or novel about Rust. It isn't. Not even the idea of safety around malloc is new, new[]/delete[] are themselves abstractions intended to make it less error prone to create/destroy contiguous blocks of memory.
That doesn't even get into the smart objects and RAII which have been around for a very very long time in C++.
None of this is new to Rust, and I think it's a bit disingenuous to imply that it is.
Right, I wasn't talking about just that, I was specifically referring to abstractions that make you change your programming patterns. C++ has those too (the move-based abstractions), but I don't consider new/delete to be them.
My point was sort of "being able to write code that looks like this is not necessary for systems programming". Being able to do the task at hand is, but it need not be done using code like that.
> This is presented as something unique to rust, which isn't even close to the accurate.
Fair. It's not. I didn't intend to mean that. I'm sorry.
I was a C++ programmer for a quite a while, some of which time I used modern C++, so I'm quite aware that these abstractions are not unique to Rust.
The reason I phrased it that way is that C++ doesn't really make it harder to use direct-malloc-based (or raw-pointers-with-new) programming, and most of the lower-level C++ code I've seen (this is anecdotal) still uses this. So C++ doesn't contest the claim the same way Rust does (it does contest the claim though), since Rust codebases doing the same thing (even the kernels in Rust) tend to move strongly in the direction of avoiding unsafe pointer twiddling as much as it can. This is a bit more nuanced (and based on anecdotal evidence on low level codebases I've seen) than I really wrote in my original comment, and I apologize for that.
I looked at Rust specs 1.0 when they were released and immediately recognized that Rust is not a 'dating' language, but rather a very serious 'marriage' till death do us part sort of deal. (I would say the same for Haskell, & Scala.) And imho the initial promise of addressing the out of control complexity of C++ was not met.
I have no doubt that Rust is a thoughtful, and powerful, language. But you have to be prepared to exchange vows at the alter.
(You should not read my OP as a negative critique. I'm merely pointing out that anyone expecting 'immediate gratification' with Rust is likely to be disappointed.)
> the compiler thinks about it for me.
Indeed. One valid approach to categorizing programming languages is by considering upfront vs amortized cognitive load.
I'm not sure. Type traits seem to be much more manageable than how the same thing is handled in C++ template code, no?
Also, if we can bring in ecosystem effects such as cargo...
Personally, I used OCaml while at university to write a programming language (http://jsjs-lang.org) and totally loved it. Real world OCaml is a great resource and I'd definitely recommend giving OCaml a try. My only regret is that I don't get to write OCaml as much as I want to due to the kind of projects I work on, but given that Bucklescript[0] and Reason[1] are moving ahead in full steam, that might change shortly too :)
It's definitely worth playing around with a language from that family. OCaml is maybe slightly more practical than Haskell and it has some nice resources available - in particular Real World OCaml looks really good: https://realworldocaml.org/
If you already know Rust I wouldn't bother learning both - they're very similar languages.
1. Go unsafe on lowest-level operations that simply can't be GC'd. Quite a few of these can be expressed as finite, state machines which can be model-checked for correctness. Static analyzers are also really good these days in whatever code you can avoid dynamic allocation in. The rest of the code is GC'd. That's what the Oberon operating systems do.
2. Use a real-time, low-latency, concurrent GC that basically knocks low single or two digit performance out of the system. Lots of products in embedded Java have special GC's for that. Buy a faster CPU. Trade dollars or performance for productivity and safety.
3. Use a mix of GC's in the system where each one is tuned for the job at hand. You might use a real-time, concurrent one in the kernel. Maybe just reference counting in one more about performance. Maybe a straight-forward one for stuff that's not performance-critical. Could even use one that's mathematically verified for correctness in that situation. JX Operating System is an example of OS that mixes up GC strategies.
That's for software. Your options open up a bit if you can also deploy custom hardware. At the least, a modified soft-core on an FPGA. The LISP machines & maybe a Java CPU of the past often included hardware acceleration for garbage collection. One built the GC right into memory subsystem/interface of the processor where neither the processor nor applications had to be aware of its existence. It just did its thing in parallel to execution of the app. A concurrent, low-latency one integrated with virtual, memory hardware shouldn't take much hardware resources to pull off.
Anyway the syntax is so similar you can learn one and switch to the other with very little pain.
I remember asking on freenode's #ocaml for help with a convoluted error message, and when I was told what it meant and how to fix it, I asked how the person who helped me knew what it meant. He told me he'd taken two semesters of type theory courses at uni. Having to spend a year taking university-level courses in order to effectively program in OCaml seemed like way too much pain for too little gain when there were so many other less painful languages to program in.
Later I discovered Lisp and then Scheme. Both were a sheer joy to use compared to OCaml. I didn't have to wrestle with the type checker, my programs were quick and easy to write and understand, and in their own way the languages were just as powerful as OCaml was. Sure, OCaml had them beat at safety (at least if you were comparing ordinary Lisps and Schemes, not statically typed versions or extensions with more safety features) but it way, way more painful and slow to program in, for me. I recognize that if you need the safety, then the pain might be worth it. But even then it might be worthwhile to program in a dynamic language like Lisp/Scheme and then once the program matures, rewrite it in a safe language like OCaml. But even then, I think I'd just rather dial up the safety features of Lisp/Scheme or use one of the static versions.
That's just me. I know some people love OCaml. And I'm sure I'm missing some of its wonders that are only apparent when you dive deep enough in to the language. But from what I did learn of it, it just wasn't for me.
I believe that we are seeing the same kind of phenomenon in Rust: we are trying to replicate C or C++ code directly in Rust, and we get frustrated when our efforts are foiled by the borrow checker. It's going to take some time and some efforts before we learn and internalize how borrowing works in Rust and how to change the way we write programs so that it is no longer an obstacle. The Rust team looks very receptive to comments about taking away some pain points (e.g., non-lexical lifetimes), but we also need to accept that, for a little while, we are going to feel like we did when we were new programmers and were trying to make sense of these new constraints.