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.
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.
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.
It's required to do any intialization, particularly for compound futures (e.g. a "join" or "select" type of combinator), since you need to be able to move the future from where it's created to where it's eventually used/polled. I assume some of those cases could be subsumed by &uninit if that existed yeah.
But if you told me that some strongly typed language wanted to have coroutines and futures, that coroutine bodies would not execute at all until first polled, and that it was okay to move the thing you have before polling not the thing you had after polling, and I hadn’t seen Rust, I would maybe suggest:
1. Creating a Future (i.e. logically calling an async function) would return an object that is, conceptually, a NotYetPolledFuture. That object is movable or relocatable or whatever you call it (or it’s movable if and only if the parameters you passed are).
2. Later you exchange that object for a LiveFuture, which cannot be moved.
Rust has two limitations that would make this awkward:
- Rust doesn’t actually have immovable objects per se.
- The exchange is spelled fn exchange(val: Type1) -> Type2, which doesn’t work if Type2 is immovable.
But the &uninit/&own proposal in the OP is actually secretly a complex scheme using lifetimes to somewhat awkwardly do an in-place exchange from an uninitialized type to an initialized and owned type. Maybe that proposal could be extended a little bit to allow type exchanges. (Maybe it already does, sort of? You could pass an &uninit Future and a FutureToken and get out an &own Future, with the caveat that this would force the fields in the token to be moved unless the optimizer did something truly heroic.)
A type that might require stable pointers, like async{}, might want to be movable prior to use, so you don't want the type to require the value be pinned immediately. Or if you do, you need a construction like pinned-init that offers `&pin out T` - a pinned place that can be written to on initialisation of the type.
This is a very insightful observation, and Niko Matsakis (leading influence of Rust's borrow checker) would likely agree with you that this is an instance where Rust's default borrowing rules are probably too permissive, in the sense that being more restrictive by default regarding the "swappability" of &mut could lead to Rust being able to provide more interesting static guarantees. See his blog post here: https://smallcultfollowing.com/babysteps/blog/2024/09/26/ove...
> 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?
See this blog post from withoutboats: https://without.boats/blog/pinned-places/ for arguments as to why pinning is properly modeled as a property of a place rather than a type (particularly the section "Comparison to immovable types"), as well as this post from Niko that ties this point in with the above point regarding swappability: https://smallcultfollowing.com/babysteps/blog/2024/10/14/ove...
> One could imagine an alternative design in which instead of places being unpinned by default and opting into pinning, places are pinned (or perhaps “immovable”) by default, and have to opt into supporting the ability to move out of them. This would make it so that by default places have the least power (can only access via shared reference) and they gain a monotonically increasing set of powers (can assign to them, can move out of them).
> In addition to places having to opt into moving, there would be three reference types instead of two: immutable, mutable, and movable references.
&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-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/b5732b9783dce5d2311...
> 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" ?
the__alchemist•1mo ago
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•1mo ago
I don't know 100% for sure. It's a bit confusing...
jojomodding•1mo ago
> 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•1mo ago
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-o...
VorpalWay•1mo ago
GardenLetter27•1mo ago
MaulingMonkey•1mo ago
maxbond•1mo ago
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•1mo ago
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.:
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•1mo ago
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, ...
VorpalWay•1mo ago
Zig might be an option in the future, and it does give more control over allocations. I don't know what the exception story is there, and it isn't memory safe and doesn't have RAII so I'm not that interested myself at this point.
I guess Ada could be an option too, but I don't know nearly enough about it to say much.
jibal•1mo ago
gethly•1mo ago
prxm•1mo ago
i never got this point. whats stopping me from writing a function like this in zig?
the only thing explicit about zig approach is having ready-to-use allocator definitons in the std library. if you excluded std library and write your own allocators, you could have an even better api in rust compared to zig thanks to actual shared behaviour features (traits). explicit allocation is a library feature, not a language feature.gethly•1mo ago
i use neither of those languages, so don't ask me for technical details :D
thegeekpirate•1mo ago
FpUser•1mo ago
C++ has a way to tell to compiler that the function would raise no exceptions. Obviously it is not a guarantee that at runtime exception will not happen. In that case the program would just terminate. So it is up to a programmer to turn on some brain activity to decide should they mark function as one or not.
alfiedotwtf•1mo ago
Sure, it would be nice to get an error, but usually the biggest threat to your system as a whole is the unapologetic OOM Killer
MindSpunk•1mo ago
goku12•1mo ago
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•1mo ago
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•1mo ago
aw1621107•1mo ago
What programming language(s) satisfy this criteria, if any?
GhosT078•1mo ago
aw1621107•1mo ago
On the other hand, I did find what I think are the relevant docs [0] while looking more into things, so I got to learn something!
[0]: https://docs.adacore.com/gnat_rm-docs/html/gnat_rm/gnat_rm/c...
cogman10•1mo ago
I can't think of any established language that doesn't fit that exact criteria.
The last major language breakage I'm aware of was either the .Net 2 to 3 or Python 2 to 3 changes (not sure which came first). Otherwise, pretty much every language that makes a break will make it in a small fashion that's well documented.
gethly•1mo ago
aw1621107•1mo ago
PHP has had breaking changes [1].
Ruby has had breaking changes [2] (at the very least under "Compatibility issues")
Not entirely sure whether this counts, but ECMAScript has had breaking changes [3].
[0]: https://go.dev/blog/loopvar-preview
[1]: https://www.php.net/manual/en/migration80.incompatible.php
[2]: https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-rele...
[3]: https://tc39.es/ecma262/2025/#sec-additions-and-changes-that...
gethly•1mo ago
aw1621107•1mo ago
kbolino•1mo ago
*1: A great many examples of synthetic code were contrived to argue against the change, but none of them ever corresponded to Go code anyone would actually write organically, and an extensive period of investigation turned up nothing
*2: As in, the original behavior of the code was actually incorrect, but this wasn't discovered until after the loopvar change caused e.g. some tests to fail, prompting manual review of the relevant code; as a tangent, this raises the question of how often tests just conform to the code rather than the other way around
aw1621107•1mo ago
kbolino•1mo ago
*: strongest competitor for "biggest noob trap" IMO is using defer in a loop/thinking defer is block scoped
aw1621107•1mo ago
SideburnsOfDoom•1mo ago
C# for instance isn't such a "small language", it has grown, but code from older versions, that does not use the newer features will almost always compile and work as before.
breaking changes are for corner cases, e.g. https://github.com/dotnet/roslyn/blob/main/docs/compilers/CS...
aw1621107•1mo ago
speed_spread•1mo ago
aw1621107•1mo ago
[0]: https://stackoverflow.com/q/1654923
[1]: https://news.ycombinator.com/item?id=28542853
cogman10•1mo ago
Java is very good here, but (and not totally it's fault) it did expose internal APIs to the userbase which have caused a decent amount of heartburn. If your old codebase has a route to `sun.misc.unsafe` then you'll have more of a headache making an upgrade.
Anyone that's been around for a while and dealt with the 8->9 transition has been bit here. 11->17 wasn't without a few hiccups. 17->21 and 21->25 have been uneventful.
your_fin•1mo ago
VorpalWay•1mo ago
Thry do reserve the right to do breaking changes for security fixes, soundness fixes and inference changes (i.e. you may need to add an explicit type that was previously inferred but is now ambiguous). These are quite rare and usually quite small.
aw1621107•1mo ago
kreetx•1mo ago
kibwen•1mo ago
jacquesm•1mo ago
I wasn't particularly commenting on Rust's backward compatibility story so if you're not sure what I was arguing about then why did you feel the need to defend Rust from accusations that weren't made in the first place?
alfiedotwtf•1mo ago
armchairhacker•1mo ago
Many people encounter these algorithms after many other people have written large libraries and codebases. It’s much easier to slightly extend the language than start over or (if possible) implement the algorithm in an ugly way that uses existing features. But enough extensions (and glue to handle when they overlap) and even a language which was initially designed to be simple, is no longer.
e.g., Go used to be much simpler. But in particular, lack of generics kept coming up as a pain point in many projects. Now Go has generics, but arguably isn’t simple anymore.
kstrauser•1mo ago
Even adding a new keyword will break some code out there that used that as a variable name or something. Perfect backward compatibility means you can never improve anything, ever, lest it causes someone a nonzero amount of porting effort.
jacquesm•1mo ago
kstrauser•1mo ago
Someone•1mo ago
I think it’s best if it is one of the first two, as that makes it easier to add third party code to your code, and will require less effort to bring newcomers up to speed w.r.t. the code. As an example, take strings. C doesn’t really have them as a basic type, so third party libraries all invent their own, requiring those using them to add glue code.
That’s why standard libraries and, to a lesser extent, languages, tend to grow.
Ideally that’s with backwards compatibility, but there’s a tension between moving fast and not making mistakes, so sometimes, errors are made, and APIs ‘have’ to be deprecated or removed.
jacquesm•1mo ago
C and personal computing hit their stride at roughly the same time, your choices were (if you didn't feel like spending a fortune) Assembly, C, Pascal and BASIC for most systems that mere mortals could afford. BASIC was terribly slow, Pascal and C a good match and assembler only for those with absolutely iron discipline. Which one of the two won out (C or Pascal) was a toss up, Pascal had it's own quirks and it was mostly a matter of which of the two won out in terms of critical mass. Some people still swear by Pascal (and usually that makes them Delphi programmers, which will be around until the end because the code for the heat-death of the universe was writting in it).
For me it was Mark Williams C that clinched it, excellent documentation, good UNIX (and later Posix) compatibility and whatever I wrote on the ST could usually be easily ported to the PC. And once that critical mass took over there was really no looking back, it was C or bust. But mistakes were made, and we're paying the price for that in many ways. Ironically, C enabled the internet to come into existence and the internet then exposed mercilessly all of the inherent flaws in C.
rayiner•1mo ago
Yes. At the very least, features should carry a lot of weight and be orthogonal to other features. When I was young I used to pride myself on knowing all the ins and outs of modern C++, but over time I realized that needing to be a “language lawyer” was a design shortcoming.
All that being said I’ve never seen the functionality of Rust’s borrow checker reduced to a simpler set of orthogonal features and it’s not clear that’s even possible.
alfiedotwtf•1mo ago
A small language but with the ability to extend it (like Lisp) is probably the sweet spot, but lol look at what you have actually achieved - your own dialect that you have to reinvent for each project - also which other people have had to reinvent time after time.
Let languages and thought be large, but only used what is needed.
jacquesm•1mo ago
If I try the same with a python project that I wrote less than five years ago I'm very, very lucky if I don't end up with a broken system by the time all of the conflicts are resolved. For a while we had Anaconda which solved all of the pain points but it too seems to suffer from dependency hell now.
George Orwell was a writer of English books, not a programmer and whatever he showed us he definitely did not show us that small programming languages constrain our thinking. That's just a very strange link to make, programming languages are not easily compared with the languages that humans use.
What you could say is that a programming languages' 'expressivity' is a major factor in how efficient it is in taking ideas and having them expressed in a particular language. If you take that to an extreme (APL) you end up with executable line-noise. If you take it to the other extreme you end up some of the worst of Java (widget factory factories). There are a lot of good choices to be found in the middle.
alfiedotwtf•1mo ago
You mean:
> small languages constrain our thinking.
:)
pjmlp•1mo ago
esafak•1mo ago
gucci-on-fleek•1mo ago
> I doubt that anybody truly knows <language>.
> Always keep the language reference with you.
> Use <tool>, the linter.
seem like they apply to all languages (and I agree that they're great advice!).
the8472•1mo ago
VorpalWay•1mo ago
jibal•1mo ago
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•1mo ago
kstrauser•1mo ago
mrkeen•1mo ago
kstrauser•1mo ago
The function could mutate foo to be empty, if foo is mutable, but it can’t make it not exist.
mrkeen•1mo ago
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"
kstrauser•1mo ago
AngriestLettuce•1mo ago
Not that this is is reasonable code to encounter in the wild, but you certainly can do this. You could even make it work properly when called from inside functions that use `fastlocals` if you're willing to commit even more reprehensible crimes and rewrite the `f_code` object.
Anyway, it's not really accurate to say that Python passes by reference, because Python has no concept of references. It passes by assignment. This is perfectly analogous to passing by pointer in C, which also can be used to implement reference semantics, but it ISN'T reference semantics. The difference comes in assignment, like in the following C++ program:
Just like in Python, you can modify the underlying object in the pointer example by dereferencing it, but if you just assign the name to a new value, that doesn't rebind the original object. So it isn't an actual reference, it's a name that's assigned to the same thing.ANYWAY, irrelevant nitpicking aside, I do think Python has a problem here, but its reference semantics are kind of a red herring. Python's concept of `const` is simply far too coarse. Constness is applied and enforced at the class level, not the object, function, or function call level. This, in combination with the pass-by-assignment semantics does indeed mean that functions can freely modify their arguments the vast majority of the time, with no real contract for making sure they don't do that.
In practice, I think this is handled well enough at a culture level that it's not the worst thing in the world, and I understand Python's general reluctance to introduce new technical concepts when it doesn't strictly have to, but it's definitely a bit of a footgun. Can be hard to wrap your head around too.
mrkeen•1mo 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!
jibal•1mo ago
kstrauser•1mo ago
jibal•1mo ago
> 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)
This is not true at all. There's a big difference, for instance, between assigning a reference and assigning an object ... the former results in two names referring to the same object, whereas in the latter case they refer to copies. I had a recent case where a bug was introduced when converting Perl to Python because Perl arrays have value semantics whereas Python lists have reference semantics.
There seem to be disagreements here due entirely to erroneous or poor use of terminology, which is frustrating ... I won't participate further.
mrkeen•1mo ago
> 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.)
Then let me just FTFY based on what you've said later:
Python will not be very clear about this ... everything, lists, class instances, dicts, tuples, strings, ints, floats, they all require the programmer to read and write documentation every time.
jibal•1mo ago
I won't comment further.
jibal•1mo ago
kibwen•1mo ago
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•1mo ago
jacquesm•1mo ago
dwattttt•1mo ago
cyberax•1mo ago
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.
adrian_b•1mo ago
Moreover, "continue" has been invented only in 1974, as one of the few novel features of the C programming language, some years after the Dijkstra discussion.
Both simple "break" and "continue" are useful, because unlike "goto" they do not need labels, yet the flow of control caused by them is obvious.
Some languages have versions of "break" and "continue" that can break or continue multiple nested loops. In my opinion, unlike simple "break" and "continue" the multi-loop "break" and "continue" are worse than a restricted "goto" instruction. The reason is that they must use labels, exactly like "goto", but the labels are put at the beginning of a loop, far away from its end , which makes difficult to follow the flow of control caused by them, as the programmer must find first where the loop begins, then search for its end. Therefore they are definitely worse than a "goto".
Instead of having multi-loop break and continue, it is better to have a restricted "goto", which is also useful for handling errors. Restricted "goto" means that it can jump only forwards, not backwards, and that it can only exit from blocks, not enter inside blocks.
These restrictions eliminate the problems caused by "goto" in the ancient non-structured programming languages, which were rightly criticized by Dijkstra.
whizzter•1mo ago
There's a huge difference between reining in real world chaos vs theoretical inelagancies (ESPECIALLY if fixing that would introduce other complexity to work around the lack of it).
alfiedotwtf•1mo ago
And I don’t see anything bad about this!
After 11 years of full-time Rust, I have never needed to use Pin once, and it’s only having to do FFI have I even had to reach for unsafe.
Unless you memorise the Rust Reference Manual and constantly level up with each release, you’ll never “know” the whole language… but IMHO this shouldn’t stop you from enjoying your small self-dialect - TMTOWTDI!
akoboldfrying•1mo ago
As someone else who has learned (and forgotten) a great deal of Perl and C++ arcana: The badness is that it makes it harder for one person to understand another person's code.
nextaccountic•1mo ago
Sytten•1mo ago
steveklabnik•1mo ago
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•1mo ago
kibwen•1mo ago
steveklabnik•1mo ago
sesm•1mo ago
jaen•1mo ago
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•1mo ago
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•1mo ago
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•1mo ago
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.