Remix.run Logo
The Algebra of Loans in Rust(nadrieril.github.io)
180 points by g0xA52A2A 4 days ago | 87 comments
the__alchemist 4 days ago | parent | next [-]

I 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...

kibwen 12 hours ago | parent | next [-]

> 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.

alfiedotwtf an hour ago | parent | next [-]

Coming from 14 years of Perl, and dabbling in Perl 6, I don’t consider Rust a “large language”… but like Perl (and to an extent C++) I do find people craft their own dialects over time via osmosis.

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!

morcus 6 hours ago | parent | prev [-]

Wow, I had no idea JavaScript has labeled break! Thanks for the comment.

jacquesm an hour ago | parent [-]

It'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.

VorpalWay 13 hours ago | parent | prev | next [-]

Many 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.

goku12 13 hours ago | parent | prev | next [-]

I 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 13 hours ago | parent | next [-]

I 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 12 hours ago | parent | next [-]

Languages 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.

kibwen 12 hours ago | parent | next [-]

I'm not sure what this is arguing against here. Anyone who follows Rust knows that it's relatively modest when it comes to adding new features; most of the "features" that get added to Rust are either new stdlib APIs or just streamlining existing features so that they're less restrictive/easier to use. And Rust has a fantastic backwards compatibility story.

jacquesm an hour ago | parent | next [-]

I had C++, python and ruby in mind, but yes, GP also mentioned Rust in the list of 'sprawling' languages, and they are probably right about that: Rust started as a 'better C replacement' but now it is trying to dominate every space for every programming language (and - in my opinion - not being very successful because niche languages exist for a reason, it is much easier to specialize than to generalize).

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 36 minutes ago | parent | prev [-]

Tbh I think `rust-toolchain` solves most of these issues

kstrauser 11 hours ago | parent | prev | next [-]

Egad, no. This is how you get C++, whose core tenet seems to be “someone used this once in 1994 so we can never change it”.

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.

rayiner 6 hours ago | parent | prev | next [-]

> Languages should be small, not large.

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.

Someone 8 hours ago | parent | prev | next [-]

If you want or have to build a large program, something must be large, be it the language, its standard library, third party code, or code you write.

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.

armchairhacker 12 hours ago | parent | prev | next [-]

I suspect the problem is that every feature makes it possible for an entire class of algorithms to be implement much more efficiently and/or clearly with a small extension to the language.

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.

alfiedotwtf 39 minutes ago | parent | prev | next [-]

George Orwell showed us that small languages constrain our thinking.

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.

kreetx 12 hours ago | parent | prev | next [-]

Haskell's user-facing language gets compiled down to Haskell "core" which is what the language actually can do. So any new language feature has a check in with sanity when that first transformation gets written.

aw1621107 12 hours ago | parent | prev [-]

> Backwards compatibility is an absolute non-negotiable for programming languages

What programming language(s) satisfy this criteria, if any?

VorpalWay 11 hours ago | parent | next [-]

Rust does. You have editions to do breaking changes at the surface level. But that is per crate (library) and you can mix and match crates with different editions freely.

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 7 hours ago | parent [-]

I'd normally agree that what you say is good enough in practice, but I question whether it meets GP's "absolute non-negotiable" standards. That specific wording is the reason I asked the question in the first place; it seemed to me that there was some standard that apparently wasn't being met and I was wondering where exactly the bar was.

GhosT078 12 hours ago | parent | prev | next [-]

Ada does. It has been through 5 editions so far and backwards compatibility is always maintained except for some small things that are documented and usually easy to update.

aw1621107 11 hours ago | parent | next [-]

I'd normally be inclined to agree that minor things are probably good enough, but "absolute non-negotiable" is a rather strong wording and i think small things technically violate a facial reading, at least.

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 11 hours ago | parent | prev [-]

> except for some small things that are documented

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 12 hours ago | parent | prev | next [-]

Go, PHP, Ruby, JavaScript ... I'd say majority, actually.

aw1621107 11 hours ago | parent | next [-]

It's probably borderline due to the opt-in mechanism, but Go did make a technically backwards-incompatible change to how its for loops work in 1.22 [0].

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...

kbolino 8 hours ago | parent | next [-]

The interesting thing about Go's loopvar change is that nobody was able to demonstrate any real-world code that it broke (*1), while several examples were found of real-world code (often tests) that it fixed (*2). Nevertheless, they gated it behind go.mod specifying a go version >= 1.22, which I personally think is overly conservative.

*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 7 hours ago | parent [-]

You certainly won't find me arguing against that change, and the conservatism is why I called it borderline. The only reason I bring it up is because of the "absolute non-negotiable" bit, which I took to probably indicate a very exacting standard lest it include most widespread languages anyways.

kbolino 7 hours ago | parent [-]

Yes, I think it's also a good example of how "absolute" backwards compatibility is not necessarily a good thing. Not only was the old loopvar behavior probably the biggest noob trap in Go (*), it turned out not to be what anyone writing Go code in the wild actually wanted, even people experienced with the language. Everyone seems to have: a) assumed it always worked the way it does now, b) wrote code that wasn't sensitive to it in the first place, or c) worked around it but never benefitted from it.

*: strongest competitor for "biggest noob trap" IMO is using defer in a loop/thinking defer is block scoped

aw1621107 6 hours ago | parent [-]

Strongly agree there. IMO breaking backwards compatibility is a tradeoff like any other, and the flexibility non-hardline stances give you is handy for real-world situations,

gethly 10 hours ago | parent | prev [-]

There is no such thing as perfection in the real world. Close enough is good enough.

aw1621107 7 hours ago | parent [-]

I'd normally agree with you in practice, but since "close enough" seems likely to cover most mainstream languages in use today I figured "absolute non-negotiable" probably was intended to mean a stricter standard.

SideburnsOfDoom 9 hours ago | parent | prev [-]

Yes, most of them.

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 7 hours ago | parent [-]

The thing is that "most of them" seems incongruous with a demand for "absolute non-negotiable" backwards compatibility. If not for that particular wording I probably wouldn't have said anything.

speed_spread 11 hours ago | parent | prev [-]

Java rules here. You can take any Java 1.0 (1995) codebase and compile it as-is on a recent JDK. Moreover, you can also use any ancient compiled Java library and link it to modern Java app. Java source and bytecode backward compatibility is fantastic.

cogman10 11 hours ago | parent | next [-]

* Terms and conditions apply

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.

aw1621107 11 hours ago | parent | prev [-]

Java has had some breaking changes (e.g., [0, 1]), though in practice I have to say my experience tends to agree and I've been fortunate enough to never run into issues.

[0]: https://stackoverflow.com/q/1654923

[1]: https://news.ycombinator.com/item?id=28542853

pjmlp 11 hours ago | parent | prev [-]

Even C, we are now at C23, and I bet most folks only know "my compiler C", and not even all the extensions it offers.

esafak 9 hours ago | parent [-]

That's why it's important to get it right early in the language. The new stuff may as well not exist.

gucci-on-fleek 12 hours ago | parent | prev [-]

I don't know Rust at all, but all your comments

> 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!).

nextaccountic 9 hours ago | parent | prev | next [-]

Of that table, only & and &mut actually exist, the rest are hypothetical syntax

mring33621 4 days ago | parent | prev | next [-]

I'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 13 hours ago | parent | next [-]

The 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 13 hours ago | parent | prev [-]

Right. 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-o...

VorpalWay 13 hours ago | parent [-]

Own 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.

Sytten 8 hours ago | parent | prev | next [-]

Another 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 8 hours ago | parent [-]

&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 7 hours ago | parent [-]

If we start creating more `&... T` ptr types I wonder if a future edition changes `*const T` to `&raw T` for consistency...

kibwen 5 hours ago | parent | next [-]

I'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 5 hours ago | parent | prev [-]

Ehhh it’s already consistent: ones with & borrow, ones without do not.

the8472 13 hours ago | parent | prev | next [-]

> All of these are speculative ideas, but at this point they’ve been circulating a bunch so should be pretty robust.

GardenLetter27 13 hours ago | parent | prev | next [-]

Rust gives you no guarantees that a function won't allocate or panic though.

VorpalWay 12 hours ago | parent | next [-]

Yes that is annoying, but I don't know of any mainstream systems language that does. C and C++ can also have allocations anywhere, and C++ have exceptions. And those are really the only competitors to Rust for what I do (hard realtime embedded).

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 12 hours ago | parent | next [-]

Zig doesn't have exceptions, it has error unions, so basically functions return either a value or an error code and the caller is forced by the language to note which was returned. And instead of RAII it has defer ... which of course can easily be forgotten or mis-scoped, so it's not safe.

alfiedotwtf 32 minutes ago | parent | prev | next [-]

Tbh I’ve found that whenever I’ve hit MAX RAM, failed allocations are not the biggest problem you should be focusing at that time.

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 a minute ago | parent [-]

While this is 100% true for the system allocator, hitting OOM there you're likely hosed, it isn't true if you're using arenas. I work on games and being able to respond to OOM is important as in many places I'm allocating from arenas that it is very possible to exhaust under normal conditions.

gethly 12 hours ago | parent | prev | next [-]

For allocation, Zig and Odin. Zig is explicit and Odin is implicit.

prxm 9 hours ago | parent | next [-]

> Zig is explicit

i never got this point. whats stopping me from writing a function like this in zig?

  fn very_bad_func() !i32 {
      var GPA = std.heap.GeneralPurposeAllocator(.{}){};
      var gpa = GPA.allocator();
      var s = try gpa.alloc(i32, 1000);
      s[0] = 7;
      return s[0];
  }
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 9 hours ago | parent [-]

the explicit part is that zig forces you to import allocator of your choosing whereas odin has allocator passed as part of hidden context and you can change/access it only if you want to. hence explicit behavior vs implicit behavior.

i use neither of those languages, so don't ask me for technical details :D

thegeekpirate 3 hours ago | parent | prev [-]

You can require allocations in Odin to be explicit using `#+vet explicit-allocators`

FpUser 7 hours ago | parent | prev [-]

>"Yes that is annoying, but I don't know of any mainstream systems language that does. C and C++ can also have allocations anywhere, and C++ have exceptions."

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.

MaulingMonkey 13 hours ago | parent | prev [-]

This 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 13 hours ago | parent [-]

Is 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 12 hours ago | parent [-]

> 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 7 hours ago | parent [-]

> 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, ...

sesm 7 hours ago | parent | prev | next [-]

TypeScript has `Readonly<T>` for this purpose.

FpUser 7 hours ago | parent | prev | next [-]

>"...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

jibal 12 hours ago | parent | prev [-]

> 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 11 hours ago | parent [-]

Everything being passed by object reference just means every case is equally unclear.

  answer = frobnicate(foo)
Will frobnicate destroy foo or not?
kstrauser 11 hours ago | parent [-]

No. It can’t. It can only destroy its own reference to foo, not the calling scope’s reference.

mrkeen 11 hours ago | parent [-]

Right, 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 11 hours ago | parent [-]

Right, 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 11 hours ago | parent [-]

>> 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"

kstrauser 8 hours ago | parent [-]

Then I don’t know what you mean. If you have:

  foo = open(‘bar.txt’)
  answer = frobnicate(foo)
  print(foo)
then frobnicate may call foo.close(), or it may read foo’s contents so that you’d have to seek back to the beginning before you could read them a second time. There’s literally nothing you can do in frobnicate that can make it such that the 3rd raises a NameError because foo no longer exists.
AngriestLettuce 33 minutes ago | parent [-]

  #!/usr/bin/env python3
  import inspect
  
  def frobnicate(unfrobbed: any) -> None:
      frame = inspect.currentframe().f_back
      for name in [name for name, value in frame.f_locals.items() if value is unfrobbed]:
          del frame.f_locals[name]
      for name in [name for name, value in frame.f_globals.items() if value is unfrobbed]:
          del frame.f_globals[name]
  
  foo = open("bar.txt")
  answer = frobnicate(foo)
  print(foo)

  
  Traceback (most recent call last):
    File "hackers.py", line 20, in <module>
      print(foo)
            ^^^
  NameError: name 'foo' is not defined
Be careful with the absolutes now :)

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:

  #include <print>
  
  struct Object
  {
      char member{'a'};
  };
  
  void assign_pointer(Object *ptr)
  {
      Object replacement{'b'};
      ptr = &replacement;
  }
  
  void assign_reference(Object &ref)
  {
      Object replacement{'b'};
      ref = replacement;
  }
  
  int main()
  {
      Object obj{};
      std::println("Original value: {}", obj.member);
      assign_pointer(&obj);
      std::println("After assign_pointer: {}", obj.member);
      assign_reference(obj);
      std::println("After assign_reference: {}", obj.member);
      return 0;
  }

  $ ./a.out
  Original value: a
  After assign_pointer: a
  After assign_reference: b

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.

amluto 11 hours ago | parent | prev | next [-]

Some 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.

kibwen 10 hours ago | parent | next [-]

> Why isn’t there a variant of &mut that doesn’t allow swapping the value?

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...

oconnor663 8 hours ago | parent [-]

Yes I'm especially interested in what OP thinks about the overlap (or not?) between the ideas in this post and the ideas in this part of boats' post:

> 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.

conradludgate 11 hours ago | parent | prev | next [-]

Common wisdom is that pinning is a property of the place, not the reference or the type.

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.

yuriks 11 hours ago | parent | prev [-]

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.

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 10 hours ago | parent [-]

> 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.

yuriks 10 hours ago | parent [-]

> Is this actually useful in practice?

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.

trevortheblack 2 hours ago | parent | prev | next [-]

Am 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" ?

meisel 4 hours ago | parent | prev | next [-]

What 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 3 hours ago | parent | next [-]

Can 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 an hour ago | parent | prev [-]

Rust 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-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...

asciii 8 hours ago | parent | prev [-]

I was thinking this is some accounting primer in Rust...dang, still good stuff to know.