9 ms·
The Algebra of Loans in Rust
- the__alchemist 10mo agoI have what I thought was a broad knowledge base of rust an experience in it over many domains, but I haven't heard of most of those. Have been getting by with `&`, and `&mut` only from those tables! Incidentally, I think this is one of Rust's best features, and I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages! Incidentally, I recently posted in another thread here how I just discovered the 'named loop/scope feature, and how I thought it was great, but took a while to discover. A reply was along the effect of "That's not new; it's a common feature". Maybe I don't really know rust, but a dialect of it...
- mring33621 10mo agoI'm just learning Rust but so far, it looks like the author is proposing some of these ref types, like &own and &uninit. I don't know 100% for sure. It's a bit confusing...
- jojomodding 10mo agoThe part of the blog post where it says > What’s with all these new reference types? > All of these are speculative ideas makes it pretty clear to me that they are indeed not yet part of Rust but instead something people have been thinking about adding. The rest of the post discusses how these would work if they were implemented.
- whytevuhuni 10mo agoRight. The &pin, &own, and &uninit in the article (or rather everything except & and &mut in that table) do not exist in Rust. I have seen &pin being proposed recently [1], first time I'm seeing the others. [1] https://blog.rust-lang.org/2025/11/19/project-goals-update-october-2025/ https://blog.rust-lang.org/2025/11/19/project-goals-update-o...
- VorpalWay 10mo agoOwn and uninit have been in discussions wrt in place construction. The Rust in the Linux kernel project seems to be the motivating use case for this that really got the effort going recently.
- GardenLetter27 10mo agoRust gives you no guarantees that a function won't allocate or panic though.
- MaulingMonkey 10mo agoThis is something I do wish Rust could better support. A `#![no_std]` library crate can at least discourage allocation (although it can always `extern crate alloc;` in lib.rs or invoke malloc via FFI...)
- maxbond 10mo agoIs the juice worth the squeeze to introduce two new function colors? What would you do if you needed to call `unreachable!()`? It's a shame that you can't quite do this with a lint, because they can't recurse to check the definitions of functions you call. That would seem to me to be ideal, maintain it as an application-level discipline so as not to complicate the base language, but automate it.
- MaulingMonkey 10mo ago> Is the juice worth the squeeze to introduce two new function colors? Typically no... which is another way of saying occasionally yes. > What would you do if you needed to call `unreachable!()`? Probably one of e.g.: unsafe { core::hint::unreachable_unchecked() } loop {} Which are of course the wrong habits to form! (More seriously: in the contexts where such no-panic colors become useful, it's because you need to not call `unreachable!()`.) > It's a shame that you can't quite do this with a lint, because they can't recurse to check the definitions of functions you call. That would seem to me to be ideal, maintain it as an application-level discipline so as not to complicate the base language, but automate it. Indeed. You can mark a crate e.g. #![deny(clippy::panic)] and isolate that way, but it's not quite the rock solid guarantees Rust typically spoils us with.
- VorpalWay 10mo ago> Typically no... which is another way of saying occasionally yes. You might be able to avoid generating panic handling landing pads if you know that a function does not call panic (transitively). Inlining and LTO often help, but there is no guarantee that it will be possible to elide, it depends on the whims of the optimiser. Knowing that panicking doesn't happen can also enable other optimisations that wouldn't have been correct if a panic were to happen. All of that is usually very minor, but in a hot loop it could matter, and it will help with code size and density. (Note that this is assuming SysV ABI as used by everyone except Windows, I have no clue how SEH exceptions on Windows work.) > Indeed. You can mark a crate e.g. #![deny(clippy::panic)] and isolate that way, but it's not quite the rock solid guarantees Rust typically spoils us with. Also, there are many things in Rust which can panic apart from actual calls to panic or unwrap: indexing out of bounds, integer overflow (in debug), various std functions if misused, ...
- goku12 10mo agoI doubt that anybody truly knows Rust. And this is aggravated by the fact that features keep getting added. But here are two simple strategies that I found very effective in keeping us ahead of the curve. 1. Always keep the language reference with you. It's absolutely not a replacement for a good introductory textbook. But it's an unusually effective resource for anybody who has crossed that milestone. It's very effective in spontaneously uncovering new language features and in refining your understanding of the language semantics. What we need to do with it is to refer it occasionally for even constructs that you're familiar with - for loops, for example. I wish that it was available as auto popups in code editors. 2. Use clippy, the linter. I don't have much to add here. Your code will work without it. But for some reason, clippy is an impeccable tutor into idiomatic Rust coding. And you get the advantage of the fact that it stays in sync with the latest language features. So it's yet another way to keep yourself automatically updated with the language features.
- VorpalWay 10mo agoI feel like other languages also have the issue of complexity and changing over time. I doubt I know all of C++ post C++14 for example (even though that is my day job). Keeping up with all the things they throw into the standard library of Python is also near impossible unless you write python every day. Rust has an unusually short release cycle, but each release tends to have fewer things in it. So that is probably about the same when it comes to new features per year in Python or C++. But sure, C moves slower (and is smaller to begin with). If that is what you want to compare against. But all the languages I work with on a daily basis (C++, Python and Rust) are sprawling. I don't have enough experience to speak about other languages in depth, but as I understand it Haskell for example has a lot of extensions. And the typescript/node ecosystem seems to move crazy fast and require a ton of different moving pieces to get anything done (especially when it comes to the build system with bundlers, minifiers and what not).
- jacquesm 10mo agoLanguages should be small, not large. I find that every language I've ever used that tries to throw everything and the kitchensink at you eventually deteriorates into a mess that spills over into the projects based on that language in terms of long term instability. You should be able to take a 10 year old codebase, compile it and run it. Backwards compatibility is an absolute non-negotiable for programming languages and if you disagree with that you are building toys, not production grade systems.
- the8472 10mo ago> All of these are speculative ideas, but at this point they’ve been circulating a bunch so should be pretty robust.
- VorpalWay 10mo agoMany of the things like "&own" are ideas being discussed, they don't exist in the language yet. As far as I know only &, &mut and raw pointers (mut and const) exist in stable rust at this point. The standard library has some additional things like NonNull, Rc, etc.
- jibal 10mo ago> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages! Python at least is very clear about this ... everything, lists, class instances, dicts, tuples, strings, ints, floats ... are all passed by object reference. (Of course it's not relevant for tuples and scalars, which are immutable.)
- mrkeen 10mo agoEverything being passed by object reference just means every case is equally unclear. answer = frobnicate(foo) Will frobnicate destroy foo or not?
- kstrauser 10mo agoNo. It can’t. It can only destroy its own reference to foo, not the calling scope’s reference.
- mrkeen 10mo agoRight, but I don't care about the reference to foo (that's a low-level detail that should be confined to systems languages, not application languages) I was asking about the foo.
- kstrauser 10mo agoRight, but that reference is all the function has. It can’t destroy another scope’s reference to the foo, and the Python GC won’t destroy the foo as long as a reference to it exists. The function could mutate foo to be empty, if foo is mutable, but it can’t make it not exist.
- mrkeen 10mo ago>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages! No mention of references! I don't care about references to foo. I don't care about facades to foo. I don't care about decorators of foo. I don't care about memory segments of foo. "Did someone eat my lunch in the work fridge?" "Well at least you wrote your name in permanent marker on your lunchbox, so that should help narrow it down"
- kibwen 10mo ago> Incidentally, I recently posted in another thread here how I just discovered the 'named loop/scope feature, and how I thought it was great, but took a while to discover. A reply was along the effect of "That's not new; it's a common feature". Maybe I don't really know rust, but a dialect of it... I assume I'm the one who taught you this, and for the edification of others, you can do labeled break not only in Rust, but also C#, Java, and JavaScript. An even more powerful version of function-local labels and break/continue/goto is available in Go (yes, in Go!), and a yet more powerful version is in C and C++. The point being, the existence of obscure features does not a large or complex language make, unless you're willing to call Go a large and complex language. By this metric, anyone who's never used a goto in Go is using a dialect of Go, which would be silly; just because you've never had cause to use a feature of a language does not a dialect make.
- morcus 10mo agoWow, I had no idea JavaScript has labeled break! Thanks for the comment.
- jacquesm 10mo agoIt's a terrible feature, really. If you need a labeled break what you really need is more decomposition. I'm pretty sure that Dijkstra would have written a nice article about it, alas, he is no longer with us.
- dwattttt 10mo agoIt depends on how big or trivial the loops you need are. If they're only 3 or 4 lines, extracting their body into a function doesn't improve anything.
- cyberax 10mo agoDijkstra was wrong on this one. Breaks and continues help to keep the code more readable by reducing the amount of state that needs to be tracked. Yes, from the purely theoretical standpoint, you can always rewrite the code to use flags inside the loop conditions. And it even allows formal analysis by treating this condition as a loop invariant. But that's in theory. And we all know the difference between the theory and practice.
- nextaccountic 10mo agoOf that table, only & and &mut actually exist, the rest are hypothetical syntax
- Sytten 10mo agoAnother one that is missing in the article is &raw mut/const but it is purely for unsafe usage when you need a pointer to an unaligned field of a struct.
- steveklabnik 10mo ago&raw T/&raw mut T aren't pointer types, they're syntax for creating *const T/*mut T. These aren't included in the article because they are not borrow checked, but you're right that if someone was trying to cover 100% of pointer types in Rust, raw pointers would be missing.
- gpm 10mo agoIf we start creating more `&... T` ptr types I wonder if a future edition changes `*const T` to `&raw T` for consistency...
- kibwen 10mo agoI'm hoping that languages move away from the sigil-ified legacy of C treating pointers as special syntax and just start calling these `Ref<T>`, `PtrConst<T>`, etc.
- steveklabnik 10mo agoEhhh it’s already consistent: ones with & borrow, ones without do not.
- sesm 10mo agoTypeScript has `Readonly<T>` for this purpose.
- jaen 10mo ago`Readonly<T>` in TypeScript is almost useless, unsound and completely unsafe (as are most other things in TypeScript), and in no way equivalent to the Rust affine type system. In particular, Readonly only prevents writing to the immediate fields of the object, but doing eg `const x: Readonly<X>; x.a.b = ...` is completely fine (ie. nested mutability is allowed). If you want transitive immutability, you need a type-level function (such as `ReadonlyDeep` from `type-fest`), but then that gives terrible error messages. Also due to the bivariance of the TypeScript type system, using Readonly in combination with generics can silently and automatically cast it away, making it largely pointless for actual safety...
- FpUser 10mo ago>"...and other languages" Many "other languages", particularly ones that compile to native code in traditional way have fairly explicit ways of specifying how said parameters to be treated
- adrian_b 10mo agoFor your problem, i.e. "guessing whether a function will mutate the parent structure", the solution used by Rust is far from optimal and I actually consider it as one of the ugliest parts of Rust. The correct solution for the semantics of function parameters is the one described in the "DoD requirements for high order computer programming languages: IRONMAN" (January 1977, revised in July 1977), which have been implemented in the language Ada and in a few other languages inspired by Ada. According to" IRONMAN", the formal parameters of a function must be designated as belonging to one of 3 classes, input parameters, output parameters and input-output parameters. This completely defines the behavior of the parameters, without constraining in any way the implementation, i.e. any kind of parameters may be passed by value or by reference, whichever the compiler chooses for each individual case. (An input-output parameter where the compiler chooses to pass it by value will be copied twice, which can still be better than passing it by reference, e.g. when the parameter is passed in a register.) When a programming language of the 21st century still requires for the programmer to specify whether it is passed by value or by reference, that is a serious defect for the language, because in general the programmer does not have the information needed to make a correct choice and this is an implementation detail with which the programmer should not be burdened. The fact that C++ lacks this tripartite classification of the formal parameters of a function has lead to the ugliest complications of C++, which have been invented as bad workarounds for this defect, i.e. the fact that constructors are not normal functions, the fact that there exist several kinds of superfluous constructors which would not have been needed otherwise (e.g. the copy constuctor), the fact that C++ 2011 had to add some features like the "move" semantics to fix performance problems of the original C++. (The problems of C++ are avoided when you are able to differentiate "out" parameters from "inout" parameters, because in the former case the function parameter uses a "raw" area of memory with initially invalid content, where an object will be "constructed" inside the function, while in the latter case the function receives an area of memory that has already been "constructed", i.e. which holds a valid object. In C++ only "constructors" can have a raw memory area as parameter, and not the normal functions.)
- scuff3d 10mo agoI suspect this is fairly common. The reality is most developers don't need a lot of the features a language provides. Python for example has a ton of stuff that can be done with classes using sunder methods and other magic. I'm aware of it, but in all the years I've been writing Python I've never actually needed it. The only time I've had to directly interact with it was when trying to figure out how the fuck OpenAPI generates FastAPI server code. Which fairly deep into a framework and code generation tool.
- amluto 10mo agoSome things I occasionally contemplate: (not that I’ve ever tried to work any of these out anywhere near completely) 1. Why isn’t there a variant of &mut that doesn’t allow swapping the value? I feel like it ought to be possible to lend out permission to mutate some object but not to replace it. Pinning the object works, but that’s rather extreme. 2. Would it be safe to lend the reference type above to a pinned object? After all, if a function promises to return with the passed-in parameter intact in its original location and not to swap it with a different value/place, then its address must stay intact. 3. Why is pinning a weird sticky property of a reference? Shouldn’t non-movability of an object be a property of the object’s type? Is it just a historical artifact that it works the way it does or is this behavior actually desirable? 4. Wouldn’t it be cool if there was a reference type that gave no permissions at all but still guaranteed that the referred-to object would continue to exist? It might make more sense to use with RefCell-like objects than plain &. This new reference type could exist concurrently with &mut.
- deleted 10mo ago[deleted]
- yuriks 10mo agoFor 1, I think it's hard to make a distinction between swapping an object, vs. swapping/mutating all of its fields such that it becomes equivalent to a different object. For 3, some objects only need to be pinned under certain circumstances, e.g. futures only need to be pinned after they're polled for the first time, but not before. So it's convenient to separate the pinnability property to allow them to be moved freely beforehand. I don't quite understand the usecase you have in mind for 4.
- amluto 10mo ago> For 1, I think it's hard to make a distinction between swapping an object, vs. swapping/mutating all of its fields such that it becomes equivalent to a different object. Privacy. If an object has fields I can’t access, but I have an &mut reference, I can indirectly modify them by swapping the object. More generally, there are a handful of special-seeming things one can do to an object: dropping it, swapping it, forgetting it, and leaking it. Rust does not offer especially strong controls for these except for pinned objects, and even then it feels like the controls are mostly a side effect of pinning. > For 3, some objects only need to be pinned under certain circumstances, e.g. futures only need to be pinned after they're polled for the first time, but not before. Is this actually useful in practice? (This is a genuine question, not a rhetorical question. But maybe let’s pretend that Rust had the cool ability to farm out initialization if uninitialized objects described in the OP: allowing access before pinning sounds a bit like allowing references to uninitialized data before initializing it.) For #4, I’m not sure I have a real use case. Maybe I’ll try contemplating a bit more. Most I think that shared ^ exclusive is a neat concept but that maybe there’s room to extend it a little bit, and there isn’t any fundamental reason that a holder of an &mut reference needs to ensure that no one else can even identify the object while the &mut reference is live.
- asciii 10mo agoI was thinking this is some accounting primer in Rust...dang, still good stuff to know.
- meisel 10mo agoWhat does this sort of language complexity mean for future changes in Rust? In C++, its existing complexity makes new changes so much more difficult. Is Rust reaching a similar place?
- kibwen 10mo agoCan you be more specific? What language complexity are you referring to? Plenty of things in this post are hypothetical, not actual features being surfaced by the language.
- kmeisthax 10mo agoRust only supports & and &mut references. This was a deliberate choice early in the development of the language. &own, &pin, and &uninit are proposals for additional pointer types. They don't actually exist in the type system right now, but other parts of the compiler do have to care about them. Another blog post that floated around here about a month ago called these "inconceivable types"[0]; adding them to the type system would allow formally extending these behaviors across function boundaries. Like, right now, implementers of Drop can't actually move anything out of the value that's about to be destroyed. The Drop trait gets a &mut, but what we really want is to say "destroy this value over there". Rust's type system cannot understand that you own the value but not the place it lives in. What you need is an "owned reference" - i.e. &own, where the borrow checker knows that you can safely move out of it because it's going to get destroyed anyway. Rust also can't support constructors, for the same reason. What we really have are factory functions: you call them, they return a value, you put it somewhere. This is good enough that Rust users just treat factory functions as if they were constructors, but we can't do "placement new" type construction with them, or partial initialization. At least not in a way the type system can actually check. &pin is a first-class version of Pin<T>. Rust was originally designed under the assumption that any type can be memcpy'd at any time; but it turns out not being able to move types is actually super useful. Fortunately, it also turned out you could use smart pointers to pin types, which was 'good enough' for what it was being used for - async code. Actually, the blog post that coined "inconceivable types" was specifically talking about writing async functions without async. It turns out Future impls encode a lot of details Rust's type system can't handle - notably, self-borrows. If a value is borrowed across an await, what's the type of the variable that got borrowed from? It's really a negative type: borrowing T to get &T also turns T into !'a T that you can't access until 'a ends. Each borrowed reference is paired to a debt that needs to be paid back, and to do that you need lifetime variables and syntax to explicitly say "pay back this debt by ending this borrow's lifetime". How much of this complexity is actually needed is another question. There's a problem that each and every one of these reference types (or, anti-types) is intended to solve. Obviously if we added all of them, we'd overcomplicate the type system. But at the same time, the current Rust type system is already known to be oversimplified, to the point where we had to hack in pinning for async. And it's already kind of ridiculous to say "Well, async is all special compiler magic" because it prevents even reasonable-sounding tweaks to the system[1]. [0] https://blog.polybdenum.com/2024/06/07/the-inconceivable-types-of-rust-how-to-make-self-borrows-safe.html https://blog.polybdenum.com/2024/06/07/the-inconceivable-typ... [1] For example, async does not currently have a way to represent "ambient context" - i.e. things we want the function to be able to access but NOT hold onto across yields. That would require a new Future trait with a different poll method signature, which the current Rust compiler doesn't know how to fill or desugar to. So you have to use the core Future trait and signature which doesn't support this kind of context borrow. To work around this limiation involves a lot of unnecessarily verbose code to drop and regain context between awaits, i.e. https://github.com/ruffle-rs/ruffle/blob/b5732b9783dce5d2311b0f2eda39ea7faf6247f1/core/src/loader.rs#L634 https://github.com/ruffle-rs/ruffle/blob/b5732b9783dce5d2311...
- trevortheblack 10mo agoAm i mistaken or is there an error in the writing? > For example, if I have a &own T I can reborrow it into a &mut T but not a &pin own T. From the table can't you do both? Maybe they mean "not a &pin mut T" ?
- ewuhic 10mo agoIt tries well, but then introduces "lifetimes" in first paragraph, and does not explain it further.
- atoav 10mo agoThw one thing Rust n3eds the most is to explain the why of these semantics better. Why do you need to borrow and which elegant programs can you build with it?