> … [T]he notion of this sort of finer level of control is definitely accurate. But Rust gives up something really big to do what they do. They give up garbage collection. Within Jane Street and generally in programming, I think a lot of the time garbage collection is fantastic, right? It allows us to program without worrying about these low-level details that for most programs, we don’t need to.
> So, for instance, when I’m writing the OCaml compiler—which is written in OCaml, of course—I do want it to be performant, but I don’t really care about microsecond latency. And so, I’m happy to write my compiler in a way that allocates a bunch of memory and then when it’s done with that memory, the garbage collector comes and gets rid of it. This is really, really convenient. In Rust, we don’t have that. In Rust, we are forced to think about memory, allocation, and deallocation at every point in time. And this gives us this fine level of control, but it comes at a real cost, and it means now we’re sort of fighting against this ‘pay-as-you-go principle’ that we were talking about earlier. In Rust, for memory, you don’t pay as you go. Everyone has to pay all the time for Rust fine memory control. So we wanna keep the high-level, garbage-collected OCaml for most applications, and then when we really want to have this fine level of control, we wanna have that, too.
> And so, some of the work that we’re doing is around adding that finer level of control, but just where we need it.
Thank you for copying this over, it really helps have a proper conversation about it.
At least from this snippet, it sounds like the author hasn't used Rust much and they are mainly imagining what it would be like to use Rust. In my experience, you do not think about memory management in Rust at all, it is just deallocated when it goes out of context, it feels as automatic as garbage collection. You do need to be more mindful of ownership, but it feels very natural, if someone is not holding the potato it drops and gets destroyed (deallocated), it interacts well with our physical intuitions.
Since memory management is about determining when will a given memory region become no longer needed, thinking about ownership (and more importantly lifetimes) is for all practical purposes manual memory management.
I actually don't really think about memory all that much for most of the kinds of work that I do. RAII kinda just does it. His idea for optional GC for some data and manual memory management for other data is kinda interesting though.
> His idea for optional GC for some data and manual memory management for other data is kinda interesting though.
Most GC languages (Java, C#, Go) have the concept of memory arenas: manually managed memory allocations. Seems like what you're looking for, though not native to Rust.
You do have to think about memory a little bit in Rust with RAII, but not much.
There are two situations:
First there are cyclic data structures. For these you have to be sure to use a construct like weak reference counting pointers for things that are self referential or you will leak memory. Either that or you store links by object ID instead of pointer. The former adds a bit of cognitive overhead and the latter adds runtime overhead.
The second case is high performance code where you want to avoid allocating memory in an inner loop.
The second case comes up a bit in GC languages too though, since if you allocate without thinking you can easily write code that balloons the heap and forces the garbage collector to constantly invoke and do a lot of work to collect temporary objects. The code will work but will be slow and allocate more heap than needed.
Edit: actually embedded is a third situation where you might have to think a lot but that’s true for any language you are using.
This seems a bit unfair to me. The author is not entirely wrong, but it feels like they don't give the full picture. The truth is in Rust you often don't have to heap allocate at all and it is very good at avoiding this much of the time. Obviously, if building something like an AST in a parser, a naïve implementation is going to be calling the boxed allocator quite a bit which will have a lot of overhead, but for things like this I would just use an arena allocator (like bumpalo). So yes, sometimes you have to do a little more allocation, but I think it is greatly over stated. I do really like OCaml, however, and agree a compacting GC with bump allocator can be quite nice at times as well (and a compiler would probably be a good use case for one).
The default abstraction in Rust, in retrospect, will be considered poorly chosen. Moving, read borrowing, readwrite borrowing, copying, cloning are an unnecessary complication over what truly happens at both your hardware level, and between components of it, and on the network between machines. Everything is copied, end of the story, no modes. And if something is copied, but it doesn't change, you can defer the copy until it's changed (copy on write). When something goes out of scope, it's freed. That's it. It's so simple, but very few platforms truly go with it, despite it's pervasive underneath the platform and much simpler than anything else we're doing.
> Everything is copied, end of the story, no modes [..] (copy on write) [..] something goes out of scope, it's freed.
this fundamentally doesn't work, in no language
Because there is a fundamental difference between something containing a owning a sub-resource and something being allowed to use a resource as a sub-resource without owning it. And copying implicitly turns a onwed reference in a shared on (and in some cases). And while not all languages do express this difference, it still matter in how you use things, which things are exposed in public APIs, which things are thread save etc.
For example on networks everything is _serialize_ which in rust term is more a clone then anything else (but actually more then just a clone in rust). While in the CPU things are normally "rust-Copy copied". But not always, hardware is complicated and trying to do "what hardware does" is basically never ever a good idea for high level abstractions (not doing so is somewhat the whole point of highlevel abstractions).
As a side note: The fundamental abstraction of Rust is ownership (split into: owning, lend exclusively, lend shared) + scoped lifetimes (things get destructed when they go out of scope). Combined with semi-manual memory management.
Scoped lifetimes is needed to archive a semi-manual memory management which was a must have design decision for the goals rust tried to archive. And move semantics are a direct consequence of this choices.
The first is fundamental to pretty much any programming but not explicitly expressed in most.
The difference between Copy and Clone is also not very rust specific, in many other languages (including GCed ones) it would be the distinction between a flat and a deep clone. There is pretty much no way to get ride of the abstraction (due to ownership of nested resources) but I agree that it could have been done a bit better the in rust, not that it's poorly designed IMHO, just not perfect.
No one thing which is a sub-optimal decisions (and was known as such since they where done) are the specific traits signatures used for some of this mechanics. Through the problem is the "better" designs need features rust doesn't even fully have today and they where needed to be stabilized ~8 years ago.
This is also where I see a potential rust 2.0 (in many years), the same core compiler but a changed standard library and a automatic transpiration step which allows you to use most of the rust 1.x crates without any changes to them (i.e. like a rust edition, but with a much larger change and just mostly backward compatible).
You said the fundamental concept in Rust is ownership.
What I described is also fundamentally ownership, but much simplified.
You can use something without owning it, because to "use it" you send a message to it, and get a message back (i.e. send input, get output). There are two ways to message something in this system:
1. To own it and message it directly.
2. To own a reference identifier to it, and message your own owner to message it via the reference and return you the resulting message.
The distinction between Copy and Clone is Rust specific. It's not about deep vs non-deep, it's about whether something is moved by default or copied implicitly by default, and they wanted to copy some simpler scalars, like numbers, say, for pragmatic reason. But this odd exception, i.e. does something move or copy by default is very much a defect in Rust. It's a core behavior that differs based on whether the type implements copy or not.
In the simplified model I propose everything is copied (deep) by default, but you don't pay the cost for larger structures being copied, due to COW.
When everything you own is uniquely yours, making all copies deep copies becomes not only trivial, but also the fastest way to copy. And there's no reason to talk about "shallow copy". The concept is meaningless, you're used to it, you have a habit for it, but if people could deep copy everything, they would. "Shallow" copies are a limitation of languages where everything is wired by handles, pointers, and references in an arbitrary graph. And then a deep copy may never terminate, among other things. It's a mess.
Rust's graph is not (as) arbitrary, per se, it's stricter, but it keeps many of the same defects, in part to maintain C compatibility.
Meanwhile, your operating system has a hierarchy-based memory management (single ownership segments, at the hardware level a hierarchy of pages etc.), and COW in select places where as I noted immutability is present for some content, some of the time (like Unix forking). So does your file system.
Everything that's mature does this: DAG for immutables, TREE for mutables. Programming languages are clearly not as mature yet.
> Rust's graph is not (as) arbitrary, per se, it's stricter, but it keeps many of the same defects, in part to maintain C compatibility.
I don't thing a core requirement for the design of an language needed for it's targeted use-cases is an defect, not having it would be an defect. I guess that's just a bad choice of wording.
> It's not about deep vs non-deep
(side not I noticed my wording choice wasn't grate, with flat copy I meant copy of a continuous memory region, it could still be a nested type as long as there are no indirections).
It's all about that. Through more specifically the expected runtime cost associated with it. Copy only works for flat memory copies, is normally very cheap and in turn is automatic. Clone works for everything including deep-clones but might be very expensive and in turn is not ever done automatically and no doing any magic CoW tricks or similar was a very deliberate decision as for the use case rust was designed with in mind you do not want a Clone happen at a "unexpected" time. Sure it's not perfect e.g. there is are "cheap-Clone" types (like Rc) where I would prefer rust to automatically clone in at least some contexts.
> In the simplified model I propose everything is copied (deep) by default, but you don't pay the cost for larger structures being copied, due to COW.
But this approach has a wide variety of problem iff you want to use it for a system language. Like it being trivial to accidentally introduce potentially very expensive cloning or accidentally using a ton of unneeded memory. And yes you could come up with all kinds of mechanisms to reduce this. But defaults matter and rust by default motivates you to not do potentially expensive clones. This comes at a cost, sure. But depending on you use-case this is exactly what you want.
> It's a mess.
but a practically very useful mess which has a lot of success in difference to any of the research languages which in the past had tried to archive similar things as you stated.
Perfection isn't and was never the goal. You always have to have a compromise between usability, compatibility, maintainability, debugability and performance. And for a "system programming language" this means C-compatibility and fine grained control over when or where copies happen.
Doesn't mean it couldn't work for languages focused on other use-case, and could be suited for typical web server or app use-cases. But even through rust is used a lot for this it wasn't designed for this use case.
> Like it being trivial to accidentally introduce potentially very expensive cloning or accidentally using a ton of unneeded memory.
You're describing things COW is designed to prevent as things COW causes. There is NEVER a very expensive cloning happening at once with COW. That's the whole point of it. Everything is shared until the last moment when it can't be, and even then only small parts are copied.
Makes me think you're missing something here. Maybe not familiar with how COW is used in practice. COW is used in your operating systems' memory management, and in your file system, as I already said. Do you assume they're like "oh yes, we want to have randomly triggered very expensive cloning and use a ton of unneeded memory!"?
You may call COW a "magic trick" but this magic trick is fundamental to how your computer works. Not only is it in your file system and OS. It's also essential to virtual machines and containerization. It's essential to virtually all backup & recovery systems. Do you understand how integral COW is to the world of computing? This scary magic trick you're so afraid of?
You're fine defending a "very useful mess" and rejecting clear improvements with inadequate or outright misleading conclusions. Most people just react superficially like this to change and I'm used to it. But I'll never stop pointing it out when I see it.
As for C compatibility without copying the memory model flaws, it's possible, but it requires more thought than Rust put into it.
> You're describing things COW is designed to prevent as things COW causes.
not quite see below
> You may call COW a "magic trick"
I didn't call it a magic trick, I stated rust decided against using "magic tricks" of internally implicitly not visible doing things below the hood. Which might involve using a Cow or similar. Cow is just the tool the trick could use not the trick by itself.
> Maybe not familiar with how COW
I'm very familiar with it.
But there is a difference between using CoW manually carefully in some places and using it everywhere all the time implicitly as part of the language design.
COW prevents you from always copying everything everywhere
at the cost of on-the-fly copying things when needed
if everything is COW all the time this will mean most of the copies are not expensive because most parts of the thing you need to copy will not need to be copied because they are also cow
but there are some gotchas
first if you have it implicitly everywhere it is very easily to overlook how many copies you make, or that you make way to many and should change your code
and even with "everything COW" the cost still can be very expensive for all kinds of reasons
e.g. in some hypothetical code like `a[10] = 4` you might now have accidentally cloned a 4GiB byte buffer, COW doesn't prevent this because that 4GiB byte buffer is a single thing. Now you might come up with ways to internally split the buffer into chunks or only use lists etc. But that isn't really working for many low level system language use-cases (but totally working for many other cases, including many server programming cases).
Similar it means if you have nested types they always need an indirection to the sub-types so that you can just copy the GC or RC references (or whatever you use for memory management) when needing to make a copy of the outer type (except where an optimizer can optimize it away) which is fine, except that for a system language you often need C layout compatible types. So you would need to severely special case C-layout types.
And as far I can tell you either special case a lot of thing in a way which isolates them away from your language (things like C-layout, fundamental non-clonable types, etc.) or you need to have a implementation of the nested CoW which no longer guarantees that it's efficient at all.
Isolating things away which doesn't fit well with your language is totally an option for a general purpose language but can become very unpleasant and/or hard to use for a low level system language.
> rejecting clear improvements
I'm not rejecting anything, I'm saying that many of the reasons design decisions you called defects are fundamentally the reason why rust has such a huge success. And calling this aspects defects isn't right. It's like saying city bike without a suspension is defect or a poor decisions. They are not. But I still would prefer other approaches (like a suspension you can switch off).
Theoretical improvements are nice, but turning them into practical improvements outside of pet projects is hard.
Which is why rust mostly went with decisions where there had been a bunch of successful research and prototyping done before. And even in that aspect with rather conservative choices. I mean that is partially where the name "rust" comes from, it mostly used stuff which was so old (in research) it could have been rusted.
Because what rust pursed wasn't being a perfect language which overhauls everything. Instead it targeted a solid improvement over C/C++ for some specific set of use-cases which very intentionally does not do anything too unusual. E.g. they directly support the C-API/ABI so that it reasonably easy to include in C/C++ projects and include C/C++ projects into it, so that you can replace some components of larger C++ projects with rust.
> As for C compatibility without copying the memory model flaws, it's possible,...
You said splitting entities into small chunks doesn't work for low-level. But as you already know (exposition monologue follows :D) memory, all of it, is allocated in small pages. Typically 4kb of size. That's what the low level actually is.
On some hardware the page is 2MB which is called, literally "huge pages" (or "large pages"). But that's about it, bigger pages are essentially not used.
If your language can't help but see 4GB as one thing, that's a flaw in your language that it even permits that. I'm not talking about how it appears on the surface, but how it's allocated and chunked.
In data structures designed for COW a single 4GB chunk would be anomalous occurrence, basically a bug, everything is split up in structures roughly the size of a page max, or so.
So we had some truth here at the hardware and OS level, which is "never allocate things in huge chunks" but we lost it at the higher level and then we think this is just how things are and design all our languages around it. An OS may permit you to allocate more memory, but that's with the idea you'll manage this yourself, say as an arena. If you instead just store a monolithic piece of state in it, that's just ignorance, or a special case, like say large buffer for DMA, or the like.
Regarding nested types, you can still inline content, you don't NEED to add indirection. The behavior of inlining vs referencing can be determined based on content structure and should be transparent to your code. It's trivial and many runtimes do this (I suppose your next objection will be how come we have a runtime, but ostensibly even C has part of its library acting as a runtime for basic things like mem alloc).
> If you combine it with higher performance requirements and requirements like wide spread platform support etc. it's very very hard.
If you have the right foundation, writing optimizations on top of it is a lot more trivial than when your foundation is 70 years of ad-hoc cruft.
This was demonstrated by Microsoft experimenting with C# written OS (I think called Singularity) that bypassed a lot of OS and even hardware APIs in order to manage most memory operations & safety on its own. And that turned out faster than C code. Unfortunately, not compatible with the status quo. C is a nightmare.
Your filesystem and OS has absolutely different constraints, they don’t modify their objects to such a degree as most programs modify memory. So honestly, you should provide more evidence why would that work (for a low-level, systems programming language).
And don’t get me wrong, FP languages en large can be very competitive with a more imperative approach, especially when multithreading is on the table, but they achieve it through extensive compile time transformations, and on the data structure level by much smarter algorithms than plain COW.
Yes I know, and I am a huge proponent of GCd languages as there really are only a tiny handful of cases where having a GC would be a problem.
But this is out of context here, where we are talking about a language whose explicit design goal is to manually manage memory without a GC. In that respect, COW wouldn’t fit the rest of the design criteria of the language, which was my point.
There are two contending definitions for low/high level languages. The more objective one only considers assemblies low-level, but that is hardly a useful categorization.
The other one is about what can be explicitly controlled by idiomatic code, in that vein C is lower level than, say, Java/C# due to pointer arithmetics (and yes, I do know that both can actually do pointer arithmetic just fine, but I wouldn’t call it idiomatic, especially not in case of Java). Rust/C++ is on the same level, if not lower than C due to having native access to SIMD.
Agreed on your general points. When I'm writing in Rust I'm really thinking far less about memory than I would have thought.
Ownership, yes. Way more than other languages, obviously. And it can get very frustrating at time. But this also has payoffs for thinking about concurrency.
I do think that there are places where this overhead gets extremely taxing. And complicated nested trees of objects, iterating through them, etc. like you'd have in a compiler or query planner etc. is definitely one of those places. The ownership and type system constraints in Rust make its Iterator pattern actually quite obnoxious for these kinds of things.
For compilers, arena allocation is king. IMHO: arenas > GC > RAII > malloc/free.
With manual memory management (whether RAII or not), it's usually not difficult to add arenas into the mix.
On the other hand, in GC languages: many GCs only allow heap pointers to the beginning of objects, not to their interior. This means arenas can't used in those GC languages; every full collection actually has to trace the millions of AST nodes individually.
You can allocate a part of a arena to form a new arena, at least theoretically.
And in many cases for compliance you want to 1) limit the memory a specific sub-component of your code can use 2) make sure it always can have this memory 3) make sure it doesn't leak any memory ever. Arenas are probably the most reliable way to archive this.
The main problem is if the sub-component needs to allocate memory which outlives it's life time. A common way around this is that the component delegating work to it has to provide that memory (e.g. windows kernel API) but that doesn't always scale well. Another is to use another memory management for it (e.g. GC) but then it isn't enforcing compliance anymore. Etc. it's always some compromis.
Allocating within the given arena is simply an option. You can still be a component owning an arena, but delegate the allocation to an ancestor/sibling context/arena if you want that allocation to outlive you.
One of the various things I placed in the `Etc.` ;=)
But like all the other things listed it's not perfect.
For example if I just pass a unconstrained access to a other components arena to a component you can no longer "guarantee" that the component doesn't leak anything as it could use the arena to allocate things besides the thing for which it was passed it to the component. So now you need to have additional steps to assure this isn't happening if you want to make sure as much as possible that everything is compliant. Through depending on what you do this aren't necessary complicated additional steps through it really depends.
In this model you can't pass access. It's not like "here's the key and drive the Ferrari", it's instead "tell me where you want to go, and I'll drive you there. Or not."
But also rusts ownership model isn't just about memory management (more state management which includes resource management which includes memory management).
And IMHO for resource management "in general" implicit semi-manual type checks scope based resource management works the most reliably (rust ownership model is more then just RAII).
Arenas can be good too, but aren't applicable always and tend to make things more complicated to get right.
Similar for GC, it can work well in some cases, but just in some and in others it's prone to resource leakage and getting dependencies right is hard.
So in my view memory management is just a special case of resource management where arenas happen to be very widely applicable and where for GCs people put a lot of work in to "get it right".
That's an interesting point of view. I really don't buy that garbage collection has advantages for the user. Sure it has advantages for the developer of the language, like a GIL, but for the user, it's more a constraint they have to live with.
Maybe I’m missing what you are saying, but my take is that millions of Python, Java, JS, and Ruby programmers would beg to differ. Even if GC sometimes causes problems, 95% of the time it vastly reduces the friction involved with solving problems using computers.
Agreed. I started writing CGI scripts in C in 1994 then switched to Perl because of CGI.pm and because I didn't have anymore to malloc and free memory, especially in am application domain filled with string manipulation.
Then some Java, a lot of Ruby, JS, Python. Manual memory management for this kind of applications is unnecessary optimization. If there is memory pressure on servers there are cheaper ways to remove it than the developer time to manually manage memory.
It does come with a huge penalty. Want to get rid of your GC pauses? Good luck with that. Best case scenario like D you get to split your libraries in GC and non-GC, or C# where you can kinda skirt around it with structs and stackalloc.
Worst case scenario Java without value types - Rewrite it in another language.
The benefit for the user is that the software exists. While it might have not if the developer was forced to be bothered with manual memory management.
That's true of anything though. And at a meta level, the end users do benefit from faster iteration.
"Yes I could do without this tool, using my bare hands... But I created this tool and now it's easier and I'm faster at iterating. And since it is easier the production cost decreases. Which means I could charge less while retaining the same margin. "
This sounds to me like a new way of saying that languages with GC allow developers to move faster in very many cases than languages without GC which I think I largely agree with.
Indeed, Rust is ideal for writing kernels, drivers, or getting more secure software in domains where people are religiously against any kind of automatic memory management system.
Other than that, there are better options in regards to productivity.
> I think, first of all, that for a programming language to be effective, everything needs to be opt-in, right?
I disagree. You gain a lot by making things not-opt in. For example, strong typing. Strong typing really becomes powerful when everybody uses it. Another example, is purity and side effects. Purity isn’t really optional in Haskell.
Which brings another point: you could say that for side effects in Haskell, you don’t pay as you go, everyone has to pay all the time. You are basically forced to deal with the concepts of monads fairly if you want to write anything interesting.
Just like Haskell gets a lot of value in using the type system to encode side effects, Rust gets a lot of value in using the type system to encode ownership. Further, I would argue that the borrow checker provides value beyond memory usage, since a clear ownership model can have follow on effects for correctness and program organization.
Adopting a higher-level language where the programmer doesn't have to worry about as much stuff generally comes with some trade-offs. Garbage collection has downsides but for a lot of use-cases they aren't significant. Saying that OCaml has a place in a world where Rust exists doesn't strike me as a very hot take.
It is a hot take for those that keep ignoring that are languages that offer both, automatic resource management, and low level primitives for performance coding.
Which is exactly one of the subjects on this podcast episode, adding such primitives to OCaml, beyond what it already supports.
I have been used both GCed language and Rust a lot and I can't agree with the quoted put into the HN Headline here.
Sure rust borrow checker+move semantics are always there and you always have to cater to it.
But they _are not_ a memory management handling tool.
It's more like a ownership control tool.
Which is used to get some decent semi-manual memory management, but also is used to provide some very neat concurrency guarantees, and many other forms of resource management.
E.g. you don't need something like python `with` or java `try-with` it's already there as part of the borrow checker/move semantics for free.
So I would rephrase the quote to:
"For reliable state handling, you don't pay as you go, everyone has to pay all the time."
I did remove the "in rust" part because IMHO this is pretty much always the case (at least for real world non pet projects). Either your language helps with handling state to some degree, in which case you always pay some form of price for it. Or it doesn't in which case you also pay a price for it due to it now being harder and much much more prone to get messy (in my experience guaranteed to get messy in most real world projects).
Through I'm never surprised if people focused on functional programming dislike rust: Because rust is _not_ a functional language, it only has many functional elements. But the moment you try write very "functional" style code in it, you can easily end up in a world of pain.
Certain common functional abstractions do not scale well if combined with the rusts ownership model (they still tend to work well in simple use-cases). Partially due to limitations of the current implementation/language design. But also some aspects seem to be very hard to solve even on a conceptual level.
I'm not sure what you want to say, because as far as I can tell you comment isn't contradicting anything in my post, nor adding anything to it and from the tone it also doesn't seam to be a innocent "hey this is grate try it".
I would be surprised if the next major language is like that.
Because IMHO low level memory management and similar features of system languages aren't needed anymore for the majority of software even many "system components".
This title broke the HN guideline: "Please use the original title, unless it is misleading or linkbait; don't editorialize." (https://news.ycombinator.com/newsguidelines.html). On HN, it's not ok to use the title field to point out what you think is important about an article. That's one of the things we mean by editorializing.
Titles are by far the biggest influence on discussion. Most comments below are not about the article as a whole—just the cherry-picked bit of the article that the title singled out. Normally we'd change the title to e.g. "The Future of Programming - Interview with Richard Eisenberg" and remove the flags, but in this case the thread is already so skewed I don't think we can.
If you want to say what you think is important about an article, that's great, but do so by adding a comment to the thread. Then your view will be on a level playing field with everyone else's: https://hn.algolia.com/?dateRange=all&page=0&prefix=false&so...
71 comments
[ 4.5 ms ] story [ 142 ms ] threadI’m curious about the claim being made but I’m being linked to a haystack.
Edit: it begins at 25:34.
> So, for instance, when I’m writing the OCaml compiler—which is written in OCaml, of course—I do want it to be performant, but I don’t really care about microsecond latency. And so, I’m happy to write my compiler in a way that allocates a bunch of memory and then when it’s done with that memory, the garbage collector comes and gets rid of it. This is really, really convenient. In Rust, we don’t have that. In Rust, we are forced to think about memory, allocation, and deallocation at every point in time. And this gives us this fine level of control, but it comes at a real cost, and it means now we’re sort of fighting against this ‘pay-as-you-go principle’ that we were talking about earlier. In Rust, for memory, you don’t pay as you go. Everyone has to pay all the time for Rust fine memory control. So we wanna keep the high-level, garbage-collected OCaml for most applications, and then when we really want to have this fine level of control, we wanna have that, too.
> And so, some of the work that we’re doing is around adding that finer level of control, but just where we need it.
Link to 25:34: https://signalsandthreads.com/future-of-programming/#2534
At least from this snippet, it sounds like the author hasn't used Rust much and they are mainly imagining what it would be like to use Rust. In my experience, you do not think about memory management in Rust at all, it is just deallocated when it goes out of context, it feels as automatic as garbage collection. You do need to be more mindful of ownership, but it feels very natural, if someone is not holding the potato it drops and gets destroyed (deallocated), it interacts well with our physical intuitions.
Most GC languages (Java, C#, Go) have the concept of memory arenas: manually managed memory allocations. Seems like what you're looking for, though not native to Rust.
There are two situations:
First there are cyclic data structures. For these you have to be sure to use a construct like weak reference counting pointers for things that are self referential or you will leak memory. Either that or you store links by object ID instead of pointer. The former adds a bit of cognitive overhead and the latter adds runtime overhead.
The second case is high performance code where you want to avoid allocating memory in an inner loop.
The second case comes up a bit in GC languages too though, since if you allocate without thinking you can easily write code that balloons the heap and forces the garbage collector to constantly invoke and do a lot of work to collect temporary objects. The code will work but will be slow and allocate more heap than needed.
Edit: actually embedded is a third situation where you might have to think a lot but that’s true for any language you are using.
this fundamentally doesn't work, in no language
Because there is a fundamental difference between something containing a owning a sub-resource and something being allowed to use a resource as a sub-resource without owning it. And copying implicitly turns a onwed reference in a shared on (and in some cases). And while not all languages do express this difference, it still matter in how you use things, which things are exposed in public APIs, which things are thread save etc.
For example on networks everything is _serialize_ which in rust term is more a clone then anything else (but actually more then just a clone in rust). While in the CPU things are normally "rust-Copy copied". But not always, hardware is complicated and trying to do "what hardware does" is basically never ever a good idea for high level abstractions (not doing so is somewhat the whole point of highlevel abstractions).
As a side note: The fundamental abstraction of Rust is ownership (split into: owning, lend exclusively, lend shared) + scoped lifetimes (things get destructed when they go out of scope). Combined with semi-manual memory management.
Scoped lifetimes is needed to archive a semi-manual memory management which was a must have design decision for the goals rust tried to archive. And move semantics are a direct consequence of this choices.
The first is fundamental to pretty much any programming but not explicitly expressed in most.
The difference between Copy and Clone is also not very rust specific, in many other languages (including GCed ones) it would be the distinction between a flat and a deep clone. There is pretty much no way to get ride of the abstraction (due to ownership of nested resources) but I agree that it could have been done a bit better the in rust, not that it's poorly designed IMHO, just not perfect.
No one thing which is a sub-optimal decisions (and was known as such since they where done) are the specific traits signatures used for some of this mechanics. Through the problem is the "better" designs need features rust doesn't even fully have today and they where needed to be stabilized ~8 years ago.
This is also where I see a potential rust 2.0 (in many years), the same core compiler but a changed standard library and a automatic transpiration step which allows you to use most of the rust 1.x crates without any changes to them (i.e. like a rust edition, but with a much larger change and just mostly backward compatible).
What I described is also fundamentally ownership, but much simplified.
You can use something without owning it, because to "use it" you send a message to it, and get a message back (i.e. send input, get output). There are two ways to message something in this system:
1. To own it and message it directly.
2. To own a reference identifier to it, and message your own owner to message it via the reference and return you the resulting message.
The distinction between Copy and Clone is Rust specific. It's not about deep vs non-deep, it's about whether something is moved by default or copied implicitly by default, and they wanted to copy some simpler scalars, like numbers, say, for pragmatic reason. But this odd exception, i.e. does something move or copy by default is very much a defect in Rust. It's a core behavior that differs based on whether the type implements copy or not.
In the simplified model I propose everything is copied (deep) by default, but you don't pay the cost for larger structures being copied, due to COW.
When everything you own is uniquely yours, making all copies deep copies becomes not only trivial, but also the fastest way to copy. And there's no reason to talk about "shallow copy". The concept is meaningless, you're used to it, you have a habit for it, but if people could deep copy everything, they would. "Shallow" copies are a limitation of languages where everything is wired by handles, pointers, and references in an arbitrary graph. And then a deep copy may never terminate, among other things. It's a mess.
Rust's graph is not (as) arbitrary, per se, it's stricter, but it keeps many of the same defects, in part to maintain C compatibility.
Meanwhile, your operating system has a hierarchy-based memory management (single ownership segments, at the hardware level a hierarchy of pages etc.), and COW in select places where as I noted immutability is present for some content, some of the time (like Unix forking). So does your file system.
Everything that's mature does this: DAG for immutables, TREE for mutables. Programming languages are clearly not as mature yet.
I don't thing a core requirement for the design of an language needed for it's targeted use-cases is an defect, not having it would be an defect. I guess that's just a bad choice of wording.
> It's not about deep vs non-deep
(side not I noticed my wording choice wasn't grate, with flat copy I meant copy of a continuous memory region, it could still be a nested type as long as there are no indirections).
It's all about that. Through more specifically the expected runtime cost associated with it. Copy only works for flat memory copies, is normally very cheap and in turn is automatic. Clone works for everything including deep-clones but might be very expensive and in turn is not ever done automatically and no doing any magic CoW tricks or similar was a very deliberate decision as for the use case rust was designed with in mind you do not want a Clone happen at a "unexpected" time. Sure it's not perfect e.g. there is are "cheap-Clone" types (like Rc) where I would prefer rust to automatically clone in at least some contexts.
> In the simplified model I propose everything is copied (deep) by default, but you don't pay the cost for larger structures being copied, due to COW.
But this approach has a wide variety of problem iff you want to use it for a system language. Like it being trivial to accidentally introduce potentially very expensive cloning or accidentally using a ton of unneeded memory. And yes you could come up with all kinds of mechanisms to reduce this. But defaults matter and rust by default motivates you to not do potentially expensive clones. This comes at a cost, sure. But depending on you use-case this is exactly what you want.
> It's a mess.
but a practically very useful mess which has a lot of success in difference to any of the research languages which in the past had tried to archive similar things as you stated.
Perfection isn't and was never the goal. You always have to have a compromise between usability, compatibility, maintainability, debugability and performance. And for a "system programming language" this means C-compatibility and fine grained control over when or where copies happen.
Doesn't mean it couldn't work for languages focused on other use-case, and could be suited for typical web server or app use-cases. But even through rust is used a lot for this it wasn't designed for this use case.
You're describing things COW is designed to prevent as things COW causes. There is NEVER a very expensive cloning happening at once with COW. That's the whole point of it. Everything is shared until the last moment when it can't be, and even then only small parts are copied.
Makes me think you're missing something here. Maybe not familiar with how COW is used in practice. COW is used in your operating systems' memory management, and in your file system, as I already said. Do you assume they're like "oh yes, we want to have randomly triggered very expensive cloning and use a ton of unneeded memory!"?
You may call COW a "magic trick" but this magic trick is fundamental to how your computer works. Not only is it in your file system and OS. It's also essential to virtual machines and containerization. It's essential to virtually all backup & recovery systems. Do you understand how integral COW is to the world of computing? This scary magic trick you're so afraid of?
You're fine defending a "very useful mess" and rejecting clear improvements with inadequate or outright misleading conclusions. Most people just react superficially like this to change and I'm used to it. But I'll never stop pointing it out when I see it.
As for C compatibility without copying the memory model flaws, it's possible, but it requires more thought than Rust put into it.
not quite see below
> You may call COW a "magic trick"
I didn't call it a magic trick, I stated rust decided against using "magic tricks" of internally implicitly not visible doing things below the hood. Which might involve using a Cow or similar. Cow is just the tool the trick could use not the trick by itself.
> Maybe not familiar with how COW
I'm very familiar with it.
But there is a difference between using CoW manually carefully in some places and using it everywhere all the time implicitly as part of the language design.
COW prevents you from always copying everything everywhere
at the cost of on-the-fly copying things when needed
if everything is COW all the time this will mean most of the copies are not expensive because most parts of the thing you need to copy will not need to be copied because they are also cow
but there are some gotchas
first if you have it implicitly everywhere it is very easily to overlook how many copies you make, or that you make way to many and should change your code
and even with "everything COW" the cost still can be very expensive for all kinds of reasons
e.g. in some hypothetical code like `a[10] = 4` you might now have accidentally cloned a 4GiB byte buffer, COW doesn't prevent this because that 4GiB byte buffer is a single thing. Now you might come up with ways to internally split the buffer into chunks or only use lists etc. But that isn't really working for many low level system language use-cases (but totally working for many other cases, including many server programming cases).
Similar it means if you have nested types they always need an indirection to the sub-types so that you can just copy the GC or RC references (or whatever you use for memory management) when needing to make a copy of the outer type (except where an optimizer can optimize it away) which is fine, except that for a system language you often need C layout compatible types. So you would need to severely special case C-layout types.
And as far I can tell you either special case a lot of thing in a way which isolates them away from your language (things like C-layout, fundamental non-clonable types, etc.) or you need to have a implementation of the nested CoW which no longer guarantees that it's efficient at all.
Isolating things away which doesn't fit well with your language is totally an option for a general purpose language but can become very unpleasant and/or hard to use for a low level system language.
> rejecting clear improvements
I'm not rejecting anything, I'm saying that many of the reasons design decisions you called defects are fundamentally the reason why rust has such a huge success. And calling this aspects defects isn't right. It's like saying city bike without a suspension is defect or a poor decisions. They are not. But I still would prefer other approaches (like a suspension you can switch off).
Theoretical improvements are nice, but turning them into practical improvements outside of pet projects is hard.
Which is why rust mostly went with decisions where there had been a bunch of successful research and prototyping done before. And even in that aspect with rather conservative choices. I mean that is partially where the name "rust" comes from, it mostly used stuff which was so old (in research) it could have been rusted.
Because what rust pursed wasn't being a perfect language which overhauls everything. Instead it targeted a solid improvement over C/C++ for some specific set of use-cases which very intentionally does not do anything too unusual. E.g. they directly support the C-API/ABI so that it reasonably easy to include in C/C++ projects and include C/C++ projects into it, so that you can replace some components of larger C++ projects with rust.
> As for C compatibility without copying the memory model flaws, it's possible,...
On some hardware the page is 2MB which is called, literally "huge pages" (or "large pages"). But that's about it, bigger pages are essentially not used.
If your language can't help but see 4GB as one thing, that's a flaw in your language that it even permits that. I'm not talking about how it appears on the surface, but how it's allocated and chunked.
In data structures designed for COW a single 4GB chunk would be anomalous occurrence, basically a bug, everything is split up in structures roughly the size of a page max, or so.
So we had some truth here at the hardware and OS level, which is "never allocate things in huge chunks" but we lost it at the higher level and then we think this is just how things are and design all our languages around it. An OS may permit you to allocate more memory, but that's with the idea you'll manage this yourself, say as an arena. If you instead just store a monolithic piece of state in it, that's just ignorance, or a special case, like say large buffer for DMA, or the like.
Regarding nested types, you can still inline content, you don't NEED to add indirection. The behavior of inlining vs referencing can be determined based on content structure and should be transparent to your code. It's trivial and many runtimes do this (I suppose your next objection will be how come we have a runtime, but ostensibly even C has part of its library acting as a runtime for basic things like mem alloc).
> If you combine it with higher performance requirements and requirements like wide spread platform support etc. it's very very hard.
If you have the right foundation, writing optimizations on top of it is a lot more trivial than when your foundation is 70 years of ad-hoc cruft.
This was demonstrated by Microsoft experimenting with C# written OS (I think called Singularity) that bypassed a lot of OS and even hardware APIs in order to manage most memory operations & safety on its own. And that turned out faster than C code. Unfortunately, not compatible with the status quo. C is a nightmare.
And don’t get me wrong, FP languages en large can be very competitive with a more imperative approach, especially when multithreading is on the table, but they achieve it through extensive compile time transformations, and on the data structure level by much smarter algorithms than plain COW.
But this is out of context here, where we are talking about a language whose explicit design goal is to manually manage memory without a GC. In that respect, COW wouldn’t fit the rest of the design criteria of the language, which was my point.
Even C is considered high level nowadays, in regards to hardware exposure.
The other one is about what can be explicitly controlled by idiomatic code, in that vein C is lower level than, say, Java/C# due to pointer arithmetics (and yes, I do know that both can actually do pointer arithmetic just fine, but I wouldn’t call it idiomatic, especially not in case of Java). Rust/C++ is on the same level, if not lower than C due to having native access to SIMD.
D also has it as well.
Julia has native access to GPUs.
So again other definition that isn't quite right on the spot.
Yes, it is splitting hairs, and back to the point that outside Assembly everything is high level.
Ownership, yes. Way more than other languages, obviously. And it can get very frustrating at time. But this also has payoffs for thinking about concurrency.
I do think that there are places where this overhead gets extremely taxing. And complicated nested trees of objects, iterating through them, etc. like you'd have in a compiler or query planner etc. is definitely one of those places. The ownership and type system constraints in Rust make its Iterator pattern actually quite obnoxious for these kinds of things.
With manual memory management (whether RAII or not), it's usually not difficult to add arenas into the mix.
On the other hand, in GC languages: many GCs only allow heap pointers to the beginning of objects, not to their interior. This means arenas can't used in those GC languages; every full collection actually has to trace the millions of AST nodes individually.
And in many cases for compliance you want to 1) limit the memory a specific sub-component of your code can use 2) make sure it always can have this memory 3) make sure it doesn't leak any memory ever. Arenas are probably the most reliable way to archive this.
The main problem is if the sub-component needs to allocate memory which outlives it's life time. A common way around this is that the component delegating work to it has to provide that memory (e.g. windows kernel API) but that doesn't always scale well. Another is to use another memory management for it (e.g. GC) but then it isn't enforcing compliance anymore. Etc. it's always some compromis.
But like all the other things listed it's not perfect.
For example if I just pass a unconstrained access to a other components arena to a component you can no longer "guarantee" that the component doesn't leak anything as it could use the arena to allocate things besides the thing for which it was passed it to the component. So now you need to have additional steps to assure this isn't happening if you want to make sure as much as possible that everything is compliant. Through depending on what you do this aren't necessary complicated additional steps through it really depends.
But also rusts ownership model isn't just about memory management (more state management which includes resource management which includes memory management).
And IMHO for resource management "in general" implicit semi-manual type checks scope based resource management works the most reliably (rust ownership model is more then just RAII).
Arenas can be good too, but aren't applicable always and tend to make things more complicated to get right.
Similar for GC, it can work well in some cases, but just in some and in others it's prone to resource leakage and getting dependencies right is hard.
So in my view memory management is just a special case of resource management where arenas happen to be very widely applicable and where for GCs people put a lot of work in to "get it right".
Then some Java, a lot of Ruby, JS, Python. Manual memory management for this kind of applications is unnecessary optimization. If there is memory pressure on servers there are cheaper ways to remove it than the developer time to manually manage memory.
Leave C for the kernel stuff, and use Perl and similar languages as much as possible for userspace.
Unfortunelly it doesn't help that many of them lack any kind of compilation, dynamic or static, thus making many reach out to C and derived languages.
For me the ideal UNIX is something like Inferno/Limbo, Android,....
Worst case scenario Java without value types - Rewrite it in another language.
"Yes I could do without this tool, using my bare hands... But I created this tool and now it's easier and I'm faster at iterating. And since it is easier the production cost decreases. Which means I could charge less while retaining the same margin. "
GC meaning any kind of memory management described in CS reference bibliography, like the GC Handbook.
The original title was The Future of Programming With Richard Eisenberg.
@dang
Agreed about the title - I posted https://news.ycombinator.com/item?id=36005612.
Other than that, there are better options in regards to productivity.
I disagree. You gain a lot by making things not-opt in. For example, strong typing. Strong typing really becomes powerful when everybody uses it. Another example, is purity and side effects. Purity isn’t really optional in Haskell.
Which brings another point: you could say that for side effects in Haskell, you don’t pay as you go, everyone has to pay all the time. You are basically forced to deal with the concepts of monads fairly if you want to write anything interesting.
Just like Haskell gets a lot of value in using the type system to encode side effects, Rust gets a lot of value in using the type system to encode ownership. Further, I would argue that the borrow checker provides value beyond memory usage, since a clear ownership model can have follow on effects for correctness and program organization.
That's exactly why I hated doing IO in Haskell. All the math concepts are overhead too.
Which is exactly one of the subjects on this podcast episode, adding such primitives to OCaml, beyond what it already supports.
Sure rust borrow checker+move semantics are always there and you always have to cater to it.
But they _are not_ a memory management handling tool.
It's more like a ownership control tool.
Which is used to get some decent semi-manual memory management, but also is used to provide some very neat concurrency guarantees, and many other forms of resource management.
E.g. you don't need something like python `with` or java `try-with` it's already there as part of the borrow checker/move semantics for free.
So I would rephrase the quote to:
"For reliable state handling, you don't pay as you go, everyone has to pay all the time."
I did remove the "in rust" part because IMHO this is pretty much always the case (at least for real world non pet projects). Either your language helps with handling state to some degree, in which case you always pay some form of price for it. Or it doesn't in which case you also pay a price for it due to it now being harder and much much more prone to get messy (in my experience guaranteed to get messy in most real world projects).
Through I'm never surprised if people focused on functional programming dislike rust: Because rust is _not_ a functional language, it only has many functional elements. But the moment you try write very "functional" style code in it, you can easily end up in a world of pain.
Certain common functional abstractions do not scale well if combined with the rusts ownership model (they still tend to work well in simple use-cases). Partially due to limitations of the current implementation/language design. But also some aspects seem to be very hard to solve even on a conceptual level.
Using D only as an example, there are many others since Xerox PARC's Cedar, CLU and Algol 68 came to be.
So maybe some GC languages left to try out?
The tone is a side effect of talking about this since the mid-90's.
I would be surprised if the next major language is like that.
Because IMHO low level memory management and similar features of system languages aren't needed anymore for the majority of software even many "system components".
Titles are by far the biggest influence on discussion. Most comments below are not about the article as a whole—just the cherry-picked bit of the article that the title singled out. Normally we'd change the title to e.g. "The Future of Programming - Interview with Richard Eisenberg" and remove the flags, but in this case the thread is already so skewed I don't think we can.
If you want to say what you think is important about an article, that's great, but do so by adding a comment to the thread. Then your view will be on a level playing field with everyone else's: https://hn.algolia.com/?dateRange=all&page=0&prefix=false&so...
Edit: there's an earlier submission https://news.ycombinator.com/item?id=35993993 which I've just added to the second-chance pool (https://news.ycombinator.com/pool, explained at https://news.ycombinator.com/item?id=26998308).