Hacker News new | past | comments | ask | show | jobs | submit login
Pin (without.boats)
498 points by thunderbong 47 days ago | hide | past | favorite | 120 comments



I’ve always thought Pin was difficult to understand because it’s not explained in a clear way in the official docs. In particular, lots of documentation claims things like “Pin ensures that an object is never moved”, which isn’t true!

It’s only true if the object is not Unpin, but most normal objects are Unpin, so Pin usually does nothing. It took me a very long time to finally understand this. The set of types T for which Pin<T> does anything at all is very niche and weird and IMO this isn’t sufficiently highlighted by the documentation.


I think this is good feedback and it would be good for the docs to be clearer about this. Of course for the types that you're going to deal with pinned (futures and streams), they're a lot more likely to be those niche objects.

I also do think the documentation has improved a lot over the years. I was surprised when I checked it while drafting this that it seemed to focus on the right things pretty well; circa 2019 I remember it being a lot more focused on specifying the contract in a way that really belongs in something like the Rust reference and not the std API docs.


My take on why users find Pin difficult: by itself, it has no meaning! This is different from every other wrapper in the language (except maybe AssertUnwindSafe<T>, which basically no one uses for its original purpose). Given a Pin<&mut InnerType>, there's nothing about Pin in the language or standard library that tells you what you can and can't do with it. (Unless the InnerType declares that it's Unpin, which implies that you can do anything you can do with an ordinary pointer.)

Instead, it operates as more of a "bring your own meaning", where the provider of the InnerType further creates any number of (internally unsafe) methods and APIs to manipulate a pinned object soundly. The only purpose of Pin<P> itself is to provide a pointer with fewer "intrinsic capabilities" (e.g., swapping &muts, moving out of Boxes, etc.), so that the inner type can allow further capabilities on top of that.

I suspect it's this nebulousness of meaning that confuses people the most. It certainly took me a fair while to figure it out. All the ideas about structural vs. non-structural fields are just to facilitate popular access patterns like "this one field is just plain old data, but this other field contains an object that itself wants to be pinned".


Pin does have meaning: it means you cannot move the target of this pointer ever again (or invalidate its without running its destructor, which is what moving does that's the problem), unless the type of the target implements Unpin.

This giving up of certain rights gives other rights (such as to store self-referential values), which are the reason you give it up. This is how contracts between components just work: similarly, giving up the right to mutate through a reference allows you to alias the reference at the same time. I'm always reminded of this line from Lincoln, about a very different and much graver subject: "If we submit to law, Alex, even submit to losing freedoms - the freedom to oppress for instance - we may discover other freedoms previously unknown to us."

I do agree that the fact that you can't use those rights in safe code is an educational problem, because one can't easily demonstrate what you can do with a pinned reference except "call a poll method which the compiler has generated for you."


> Pin does have meaning: it means you cannot move the target of this pointer ever again (or invalidate its without running its destructor, which is what moving does that's the problem), unless the type of the target implements Unpin.

Sure you can: it's just that the target has to provide its own methods for it. It's perfectly valid (if pointless) to write

  struct PracticallyUnpin(..., PhantomPinned);

  impl PracticallyUnpin {
      fn unpin_mut(self: Pin<&mut Self>) -> &mut Self {
          // SAFETY: We're the ones writing the rules here
          unsafe { Pin::into_inner_unchecked(self) }
      }

      ...
      // (no other unsafe methods or impls)
  }
and then the caller can do whatever they want with that reference, e.g., moving the value. Unpin isn't a magic word: it's just a generic way for the target to indicate that pinned pointers can safely regain full capabilities.

Of course, putting a Pin around an object does further restrict what you can do with it generically in unsafe code. But I'd further count these under the umbrella of "intrinsic unsafe capabilities", which Pin removes, but which the target can later restore ad libitum. Compare the question of whether a fn replace_with(&mut T, impl FnOnce(T) -> T) (aborting on panic) is sound, which really comes down to the "unsafe capabilities" of a &mut reference.

This is not to say, of course, that the target need not be very circumspect about which capabilities it restores! It has a responsibility not to create an unsound interface that might result in UB under permitted usage. E.g., if we have a type that owns a generic non-Unpin future, then that type must follow the strictest Pin invariants w.r.t. that future. But otherwise, it's entirely up to the target type which capabilities it wants to restore under which circumstances.


There are a couple of ways to interpret this code:

1. If this were public, or if you ever move out of the reference you get from that function, you would be violating the pin contract so this code would then be invalid.

2. Since its impossible to depend on the pin contract generically and this type doesn't actually depend on it, this is really just an indirect equivalent of implementing Unpin for the type, which is safe, so this code is valid.

I do think the second interpretation is correct (and I think its what the UCG group has decided), but this is a really nuanced conversation about the interpretation of unsafe code and validity. You started this thread by saying this is the reason pin is difficult for users: I am completely certain the median user is not in the weeds about what is and isn't valid unsafe code in pointless hypotheticals; the totally different set of idioms to get the same behavior for pinned references as for ordinary references is a much more pressing issue.


> I am completely certain the median user is not in the weeds about what is and isn't valid unsafe code in pointless hypotheticals; the totally different set of idioms to get the same behavior for pinned references as for ordinary references is a much more pressing issue.

I agree that the latter is indeed a pressing issue. But my point is that the former isn't about unsafe code so much as safe code: safe users of a concrete pinned target type may observe any number of different API surfaces, some of which allow modifying or swapping out various parts of the pinned object.

For a practical example of this, when the target type uses one of the pin-projection crates, it has a choice in which fields to denote as structural or non-structural, which affects how much the user can modify each field down the line. Indeed, a type could usefully choose make all of its fields non-structural, if only its address is externally referenced.

The pinning invariants simply don't fully constrain what the user of the target type can and cannot do, except for those constraints needed for the target type's own soundness.


In my next post, one of the major features I write about is pinned projection, and its true: for any type that supports pinned projection, it needs to decide for each field if pinning is transitive or not. This is different from how mutability works (mutability is always transitive).

There is a more elegant design which is basically reversed from pinning, but that's backward compatible with Rust. I also describe that in the next post. This pinned fields vs unpinned fields aspect of the behavior is the "necessary accidental complexity" of existing in the context of all in which we live and what came before us.


Upvoted just because the last sentence made me laugh. Perfect usage of that meme.


I've been programming Rust professionally for several years now and being totally honest I don't really understand Pin that well. I get the theory, but don't really have an intuitive understanding of when I should use it.

My usage of pin essentially comes down to "I try something, and the compiler complains at me, so I pin stuff and then it compiles."

It's never been such a hurdle in the day to day coding I need to do that I've been forced to sit down and truly grok it.


Same. It's one of my most common cases of "Just avoid unsafe and be glad some smart compiler folks already solved this all for me"

Whereas in C++ I was regularly treading in the water of "stuff I don't know, but must use" and getting eaten by crocodiles


When teaching, to make it clear that an "Unpin" item is unaffected by "Pin," I’d suggest analogies from real life where objects remain unaffected despite the use of something designed to hold them in place:

1. Velcro hooks do not stick to smooth surfaces: Pin -> Velcro, Unpin -> Smooth

2. Magnets do not affect non-magnetic materials: Pin -> Magnet, Unpin -> NonMagnetic/Glass/Brass

3. Glue does not adhere to non-stick surfaces: Pin -> Glue, Unpin -> NonStick

This way, it becomes clear that a "Velcro" fixes an item in place, and if an item is "Smooth," it is unaffected by the "Velcro" mechanism.

Given Rust’s ecosystem naming themes it would have been beautiful to rename the trait to something something magnet and non-magnetic :’)


But a smooth object can't be velcroed, nor can wood hold up a magnet.

Isn't it that `Unpin` means the object is always ready to be pinned? (I read the article last night and already forgot whether pinning requires a fixup step)

So a `T: Pin + !Unpin` is like a sheet of paper that can only be fixed by stapling it, but a `T: Pin + Unpin` is like a painting with a hook which can be mounted on a nail and unmounted without damaging the hook


I think two things fuck with our brains here:

- the double negative of „!Unpin”

- Rust trait names are not adjectives

Pin + Detach(-able) would be a less confusing name.

That being said, you can velcro smooth objects all you want, but you'll still separate (Move) them easily :) or put a magnet to glass/brass objects.

PS mind you that Pin is not a trait, only Unpin is, so Pin<T: Unpin> or Pin<T: !Unpin> is a more accurate way or writing what you described :)


> The term “value identity” is not defined anywhere in this post, nor can I find it elsewhere in Mojo’s documentation, so I’m not clear on how Modular claims that Mojo solves the problem that Pin is meant to solve

I don't claim to know the answer either, but it reminds me of a great talk from Dave Abrahams, who worked on the value semantics for Swift together with Chris Lattner (who started Mojo). The talk is "Value Semantics: Safety, Independence, Projection, & Future of Programming" [0]

[0] https://www.youtube.com/watch?v=QthAU-t3PQ4


It's clear that Mojo is in some sense inheriting Swift's notion of "value semantics," but Rust also has "value semantics" in the same sense. Rust just also has references as first class types, whereas Swift (and as far as I can tell, Mojo) only allows references as a parameter passing mode; Mojo expands on Swift's inout parameters by having an immutable reference passing mode as well.

Not being able to store references in objects does solve the problem of "self-referential structs" in that you just can't implement code like the code Rust compiles to, but that isn't at all what the quoted paragraph says about Mojo so I am quite lost as to what they mean.


My understanding of _value identity_ refers to the `StableDeref`/yoke approach to self-regerential structs. The value is constructed at a stable address (usually some heap allocation) and you always access it through some pointer. The address is the value's identity. The pointer can move, but the value doesn't move.


Could you link to a source for this in Mojo's documentation? This would be a logical interpretation, but it would mean Mojo is planning to adopt a much worse implementation of async than Rust and the post is claiming that Mojo is both faster and easier than Rust.


I'm not familiar with Mojo, so my understanding above was based on their blog post that you linked, and assumptions based on context. Checking their website, I find a sentence equating "identity" with "having an address" though:

> So far, we've talked about values that live in memory, which means they have an identity (an address) that can be passed around among functions (passed "by reference").

Source: https://docs.modular.com/mojo/manual/lifecycle/life#trivial-...

---

If their self referential structs require indirection, I agree that they're weaker then what's available in Rust. Hopefully they provide more details at some point. The "No pin requirement" section in particular focused on Mojo's async ergonomics, not Mojo's async perfs.


The problem as I see it is that having a &mut reference to something lets you move it via mem::swap/replace (and maybe a few others?) -- but actually needing to do so is rare. It seems to me that if that weren't allowed, taking a &mut reference to a self-referential value would be perfectly safe.

Maybe there could have been a way to opt in to moving-via-reference for those rare cases when you need it. Or maybe this whole problem could have been avoided by making swap and replace unsafe? I'd love to see someone explore that design space.


This is true. When were working on the problem, the way Aaron Turon put it was that &mut is "too powerful." If &mut didn't give the power to move out of it, the whole design would be a lot simpler. I'll discuss this in my next post about this.

Rust has to be backward compatible and already decided you can move out of an &mut, but there are definitely cleaner designs that can be, unburdened by what has been.


This just doesn't scale and it would have been a non starter because it would break so much exiting code. Mem swap is just one of the ways to move via mutable references, there are many many others. Option::take is one that I use quite often and it would be super weird if that was unsafe.


Great seeing this backstory. WithoutBoats has had some super active discussions around very topical async iterators, poll, and pin topics already! https://news.ycombinator.com/from?site=without.boats

It feels like there's very few communities that people going so in depth publically about the nitty gritty of their language, and it's so cool to see.


Its cool, but it also means the development of the language is very very slow. Async is still half backed and super complex, and I say that as a guy who has written rust code 40h/week for the past 3 years.


This isn't why development is slow. In my opinion, if I were still employed to work on Rust improvements to async would have shipped a lot faster. My blogging about it in my free time is my effort, in light of my circumstances, to get the project back to shipping on async.


It would help for sure to have a lead on this. Still there are a lot of opinions on the way forward for async so it would still be slow I think.

I did get annoyed recently by the trait async stabilization that promised us a good trait-variant [1] which has been abandoned. Makes it so much harder to build a library without it.

[1] https://github.com/rust-lang/impl-trait-utils


For someone not in the loop, why not go back into it and take the lead? At least on a part time, volunteer basis.


I fully agree. Sometimes it feels like so much effort is put into rust's async side that the rest of the language ends up taking a back seat and suffering for it.


Rust dev team cares too much about async and webdev imho. Yes I know the web is where 95% of programming jobs live now. But as a C++ systems programmer I simply could not care less about the web. Rust is a systems language trying to get its foot in the web door. I'd selfishly rather it focus on being a superior C/C++. Alas.


The usual complaint is that there has been little progress on async and webdev in Rust itself since 2018 so it's odd to hear the reverse complaint.

Though I would add that Rust's async is not just about webdev; it has had success in embedded contexts e.g. the popular https://github.com/embassy-rs/embassy?tab=readme-ov-file#emb...


The nature of open source is that people work on what they want to work on. People work on async because it interests them. But to the same level many people in the lang and libs team don't know async that well. You may perceive it that everyone works only on async but this is just not true.


Async is not for webdev only.


It feels like async is popular and necessary but just not that good fit for rust. Having it is better than not having it I suppose....


I can imagine a Rust-like language where we have move-constructors (in TFA), and every generated Future subtype is opaque and also heap allocated for us.

I think the need for Pin could then disappear, because the user would have no way to destroy it, since it's opaque and elsewhere on the heap, and therefore no way to move it (because having move-constructors implies that moving is conceptually destroying then recreating things).


Pin is a state rather than a property of the data itself.

This has a very nice effect of allowing merging and inlining of Futures before they're executed.

It's similar to how Rust does immutability — there's no immutable memory, only immutable references.


You don't need move constructors if every future is heap allocated. But then every call to an async function is a separate allocation, which is terrible for memory locality. Some kind of virtual stack would be much better than that (but then you need garbage collection if you want the stacks to be memory-optimized to be small by default).


You could use an actual stack. As I understand it this was not done for questionable reasons relating to borrows of thread-locals. You could also allocate a top-level async function and all of its transitive async callees all at once, if you force the user to put all this information in 1 translation unit. Or you could use a bump allocator specifically for futures used in a certain part of the program, if you're willing to give up using a global allocator for everything. So it seems like there are a lot of options.


All this is fine and dandy for async web apps but would make things like async-as-state-machine, which powers a lot of rust embedded development, basically a non-starter.


In embedded development it's impossible to use any allocation strategy other than the language constantly calling a global malloc on your behalf?


Normal async doesn’t require any malloc.


Great! I certainly didn't intend to say that stack-allocated or statically-allocated futures should somehow stop working?


If you're allocating a call stack per thread you might as well just use actual threads.


This obviously doesn't work. Maybe you can try refactoring a console emulator that uses its stack switching primitive millions of times per second to use threads instead and let us know how well it runs.


But you lose the async/await syntax (which I'm addicted to because it makes blocking point explicit, like `?` makes error explicit).


Yeah. I can guess how disruptive it would be, but I really wish rust bit the bullet and added a Move trait to std, baked into the language at a similar level as Copy. Move defines a function which moves a value from one address in memory to another. Structs without impl Move cannot be moved.

Almost all types would #[derive(Move)], which implements a trivial move function that copies the bytes. But this opens the door to self-referential types, futures, and lots of other things that need more complex move behaviour. (Actually, it might make more sense to define two traits, mirroring the difference between Copy and Clone. One is a marker trait which tells the compiler that the bytes can just be moved. The other allows custom "move constructor" implementation.)

I want move because pin is so hard to understand. Its a complex idea wrapped in double- or sometimes triple negatives. fn<X: !Unpin>(...). Wat? I drop off at unsafe pin-projecting. When is that safe? When is it not? Blah I'm out.

Moving from rust-without-move to rust-with-move would be inconvenient, because basically every struct anyone has written to date with rust needs #[derive(Move)] to be added. Including in std. And all types in existing editions that aren't pinned would need the compiler to infer a Move trait implementation. This should be possible to do mechanically. It would just be a lot of work.

Async rust is horrible. Especially compared to futures / promises in almost any other language. At some point, someone will make a new rust-like systems language which has an improved version of rust's memory safety model, a Move trait, and better futures. I'd personally also love comptime instead of rust's macro system.

I love rust. I love all the work the team has put into it over the years. But the language I’m really looking forward to is the language that comes after rust. Same idea, but something that has learned from rust’s mistakes. And it’s increasingly becoming clear what that better rust-like language might potentially look like. I can't wait.


I very much disagree that Pin is as hard as everyone makes it out to be. Using the pin! macros, the pin-project crate, and enough as_mut() to get it to compile and it's not hard at all to get a future impl working. It would be good to get this native (which is what boats wants) so it's easier to discover but it's not at all hard by any means

I think a lot of people think pin is confusing but don't actually try to learn it. When I've sat with people and helped them they understand pretty quickly what pin solves and how it works.

I very strongly think move constructors would be even more complex than pin.


I can only speak from my experience, but I really struggled with it. Ok, I understand moving. Pin is ... not moving. But the struct can still move, just ... not when you have a pointer to the struct. Ok, weird, but ok. Pin has a weird, limited set of functions to access the data fields of the struct. Some are unsafe. And then there's Unpin, which sounds like its not-not-move, so, something can move? No. From std:

> Implementing the Unpin trait for T expresses the fact that T is pinning-agnostic: it shall not expose nor rely on any pinning guarantees.

So, ??. Then there's macros for pin-project, which most projects in the wild use, but some are unsafe. Why? Which of my fields can I safely expose using pin-project?

I tried to implement a custom SSE-style streaming protocol over HTTP, to make a rust server implementation of Braid. I spent about a week trying, including pouring over the implementations of server-sent events and websockets in one of the HTTP libraries. Ultimately I failed to get it to work. (This is before TAIT and some other recent features, so things are probably be better now.)

I picked up javascript to write my server instead, and I had the protocol implemented about 20 minutes, in just 20 or so lines of code.

I adore rust, and I'd much rather a rust implementation than one based on nodejs. But I ran into skill issues here. Pin and futures in rust are hard. Or at least, I found them hard. I'm sure if I took another crack at it I'd be able to figure it out. But I don't want to spend so many of my brain cells on the language. I want to spend my attention thinking about my problem domain. Like I can in javascript.

Rust is an amazing language. But yeah, I really think that pin doesn't meet the standard that the rest of the language sets. I think it could use a rethink.


I'm curious why you needed to deal with `Pin` instead of using async functions. What led you to a path in which you needed to implement poll methods yourself?

For what it's worth, all of the practical problems you encountered with using Pin are exactly what my next post is to show how to solve.


I look forward to your next post on the topic then!

> I'm curious why you needed to deal with `Pin` instead of using async functions.

The protocol I was trying to implement streams messages over time over a single HTTP request thats kept alive for a long time. This is how Server-Sent Events (SSE) works, and its how Google Chat in gmail was first implemented in a way that supported IE 5.5 (!!!).

This was a couple years ago now, so the details are a bit fuzzy. And I was relatively new to rust at the time. I was, at the time, still sometimes surprised by the borrow checker.

My goal was to make a writable async stream that I could push messages into from other parts of my program. And it also needed backpressure. When you sent messages into the stream, the protocol implementation it encoded them and streamed them into the body of my HTTP response object. I was (I think) using hyper.

This is before TAIT was in rust, and for one reason or another I needed to store / reference the future object I was making. (If you use an async fn(), you don't get a name for the Future type the function returns. So I couldn't put the return type in my struct, because I couldn't name it.)

So I ended up writing a custom struct that implemented Future, so I could reference the future elsewhere in my code. Hence, implementing Poll myself. I can't honestly remember how Pin came into it all. I think hyper's API for doing this sort of thing stored a Pin<T> or something.

I remember at some point trying to write a where clause using higher ranked type bounds to describe the lifetime of a future object that was- or wasn't- associated with the lifetime of the corresponding HTTP request. And that may or may not have been Pinned, and I gave up.

It might be fun to revisit this at some point now rust's async support has matured a little. And now that I've matured a lot in how I understand rust. I certainly don't imagine that everyone using async will run into the sort of quagmire that I hit. But this was the first thing I ever really wanted to do with async rust, and it felt horrible to fall on my face trying.


Thanks for your write up. This sounds like a perfect use case for async generators (which yield many times and compile to Stream instead of Future), a feature I hope Rust will gain in the next year or two. To receive messages from other parts of the program, I would have the async generator hold the receiving end of a channel.


Yeah, I would love generators (and async generators) to make it into stable rust. They would be a godsend for my work.


When I worked on Fuchsia and we had a heavily async set of system interfaces, users went through this learning path very regularly, and it was very painful for many. Folks who reached out for help early in their first engagement on this kind of path got help and following a "learn by doing" started to understand what was going on after a few iterations of the same challenge. Those who struggled trying to figure it out all on their own had a really awful time and in one example even went back to c++ for a sizable project because of the wall they ran into. There's a big gap here for folks who want to self-help their way through this. TAIT reduces the number of cases that come up, but there are still plenty.

Reflecting on a point from the article, it's possible that the ?Move being required in every declaration might have been better on this aspect. The point here about not being able to remember where the requirement to deal with Pin comes from is an indicator: the virality of the key traits involved, along with implicit implementation is a particularly tricky mix, it leads to action at a distance, which is also why first time engagements are so hard for users. Mix in some misunderstandings and you're in nope territory.


Thanks for saying so. Its nice to know I'm not alone in struggling with this stuff. Now that some time has passed, I might loop back and take another stab at it. It'd feel great to clear this hurdle.


> Async rust is horrible. Especially compared to futures / promises in almost any other language.

Having written a bunch of async code over 20 years (first exposure was twisted in the early 2000s and all sorts of stuff since - including manual function/stack wrangling on my own reactor), async in rust is like many other things in rust: It forces you to deal with the problems up-front rather than 6-12 months later once something is in prod. It helps to stop and understand why the compiler is yelling at you - for me anyway once I do grok that I'm glad I didn't have a repeat of $BUG_THAT_TOOK_DOWN_PROD_FOR_A_WEEK - that was a bad week.


Another great WithoutBoats writeup. Admittedly, this is one of those corners of Rust that I'm glad is abstracted away and buried in async runtime internals.

That said, I'm curious if/when/why/how anyone uses Pin<T> outside of a custom Future implementation?


I am curious about this as well.

The way it's phrased it makes me understand a bit that it could be used within FFI, like if you get a pointer from a extern function that returns a *mut T, I could have better semantics by wrapping it with Pin<&mut T>, but then it says

"Another fact about the pinned typestate is that for most types it is completely irrelevant. If the value of type can never contain any self-references, pinning it is useless"

Mind you I'm very new at FFI but I'd like to understand what are the best ways to wrap them into safe Rust.


Ffi: you have a c api that exposes an item by ptr not by ref so it mustn't be moved.

Interacting with system types that rely on their address. Eg I think std uses a pin equivalent for mutex/futex on some operating systems because the kernel docs say the userland lock object must not change its address after initialization (even when not locked, which is normally the only condition and doesn’t need Pin because you can’t move something that has a reference to it).


Such an insane amount of work to avoid fixing the real problem: the inefficiency of threads.

All async code -- all of it -- is a hack to implement lightweight threads through a lot of syntactic sugar for state management. In a language like Rust it adds a ton of complexity that simply doesn't need to exist without it.

Fixing the efficiency and scaling issues of threads would make all of this just go away. Poof. Gone.

It's a bit like the trillion dollar mistake of "null" in languages like Java-- a ton of complexity that results from one design decision (or in this case lack thereof).


Threads don't support cancellation in any reasonable way. Cancellation is immensely useful for networked applications and for GUIs.

Threads make it difficult to fully use CPU and network, without over-subscribing either one. If you start handing off tasks between threadpools, you're on the path to reimplementing futures (or you work on callbacks/events, which make the code fragmented, and which async/await was meant to be a syntax sugar for).

The alternative for cancellation and timeouts requires weaving a Context object like golang does, and then having issues with leaf code naively calling functions that don't obey the Context properly, which is only marginally better than pains with non-async functions in async code.


Everything you're describing goes back to problems with thread APIs. I agree that threads as implemented are very coarse grained and limited. What I dislike is the idea of inflicting an explosion of cognitive load onto the programmer to manually manage things instead of thinking about how to implement a better kind of thread.

The latter is what Go did. I'm not a giant Go fan but not having async is far and away the best thing about the language and makes up for almost all its other faults.

Anything the language makes the programmer think about detracts from the programmer's ability to think about the actual problem they are solving.

BTW Rust is still superior to C++ and I use it, so I am not dissing Rust too badly. The async cancer is found across the entire ecosystem, not just Rust, so my comment was more against async programming in general as opposed to getting threading right. If we could get threading right we would massively simplify all programming everywhere.

Ultimately it boils down to the fact that it's 2024 and we still run everything on 1970s operating systems.


I don't get how can you think Golang does it better.

Its async is not easily composable. Anything beyond async 101 becomes a tangled web of DIY goroutines, wait groups, channels, callbacks, special empty signal channels for selects, and the Context objects which need careful management to be shared but not shared too much to cancel and time out right parts at the right time.

And then all of that has to be thread-safe, even though golang built-in types aren't. You need to pay sync overhead even for I/O-bound workloads or thread-per-core designs, because you don't control the fat runtime, and some of your green threads may be real.


I suspect that most the hate for Rust's async is from overuse of tokio's multi-threaded spawn(), as if it was the `go` of goroutines.

Tokio's mt spawn adds burdensome Send and 'static requirements, and often gives too fine granularity, especially if the tasks still need to return results to their caller.

spawn() is often replaceable with join_all and streams. These allow same-thread temporary data, and are runtime-agnostic.

The other self-inflicted pain is stubborn avoidance of boxed futures. Every escaping pointer in golang is heap allocated, and nearly everything goes through interfaces. If you allow Box and dyn that often in Rust, you can skip a lot of the type system and lifetime complexity too.


I doubt rewriting the Linux kernel to "fix the efficiency and scaling issues of threads" is possible, but even if it is, the Rust experts who figured out how to get Pin to work are presumably not the same as the set of kernel experts who would be capable of doing so. So what do you think they should have done, concretely? Just thrown up their hands and said "well, we won't add async to our language, because in theory someday someone might fix Linux to make threads magically fast" ?


Marking the difference between a function that synchronizes with a concurrent process and a function that does not is good, actually.


If I could ask independently of the sentiment of this thread - I am genuinely curious: Why is marking the difference good? (sorry this is only tangential to the article)


The behavior and performance of asynchronous function depend on the behavior and performance of concurrent processes, making them more complex to reason about, test and be confident in their correctness and well-suitedness.


Unfortunately crossing the user space barrier would cost something regardless of how lightweight "threads" would be. Also, making the OS the scheduler for all async tasks would preclude different scheduler designs since every runtime would have to use the OS's scheduler.


This.

The harmful decisions Rust made highlight its ingrained culture of doubling down on previous mistakes at all costs.

There seems to be no reevaluation of the cost/benefit ratio once the "preferred approach" turns out to be non-viable.

"We want to have feature X, consequences be damned" is rarely a winning move in language design.


I think he's talking about OS threads... it has nothing to do with rust's decisions.


It has everything to do with Rust's decisions.


Talking about a "decision" only makes sense if there's a reasonable alternative.

Do you think "actually fixing" OS threads was a reasonable alternative? What would you prefer for a high performance abstraction instead of async?


I think there are some lessons that can be learned from Java's virtual thread approach – note that there is a huge gap in requirements and design trade-offs, especially around embedded, when compared to Rust.

I'd just wager that the effort of getting something like that into shape is smaller than the costs of async/await.

(And no, Rust's playing with green threads once 15 years ago and failing due to quality-of-implementation issues is not an excuse to dismiss everything that came after it off the bat.)

Though it's probably not worth discussing this whole topic with Rust fans currently:

Many made async/await part of their personality and have little experience to offer besides breathlessly pointing to one of the half dozen blog articles trying to defend async/await.

It will take a few years until that language feature runs through all stages of grief and one can have an adult discussion about it.


True but I didn't just mean Rust... I meant virtually all async coding as a pattern.


Agreed, it's just that in Rust async/await hurts more than in e. g. JavaScript where the browser gives you enough hooks to have a "fresh start with(out) async".


Despicable comment and false.


Is it 'despicable' just because you don't like what it says? You didn't do anything to refute it.


I've written thousands of words on my blog about the design of async Rust, in which I carefully explain every decision and discuss the strong and weak points. This person regularly post rude low-effort comments like this one. My body of work should be enough to refute the idea that all I'm doing is doubling down.


This person regularly post rude low-effort comments

https://news.ycombinator.com/item?id=41034778

https://news.ycombinator.com/item?id=41004954

My body of work should be enough

This is a separate discussion, maybe you can put some of that body of work here.


This is literally a thread about a post on my blog. If you can’t find my body of work, I cannot help you.


It's not about me finding anything, I'm saying that your expectations probably need to be adjusted. It seems like you thought you would post a one word title and have everyone just post compliments with no questions.


I didn't ask for this to be posted on Hacker News, genius. My engagement with this community is a generous use of my free time, but I do not suffer trolls and cranks like simon-o.


My engagement with this community is a generous use of my free time

I think this attitude is a major problem. You wrote something public and here you have lots of attention and engagement and you think you're being "generous" by replying to people. If you don't want people to read it, take it down. If you don't want to engage with people on hacker news, just ignore the whole thing. Calling comments "despicable" with no explanation is not a great reaction to people giving your article the attention you wanted in the first place.


Not to mention that this boats gentlemen is on account number 3 already, so it appears that moderators on this site have already told –him more than once– that he should offer his "generous donations of free time" somewhere else if he cannot adjust his behavior.

(His older accounts show the same patterns of throwing childish tantrums and blowing up on random people.)


https://news.ycombinator.com/item?id=33512918

You weren't kidding. It's the exact same attitude of focusing on authority rather than explaining things technically.


This comment contains a link to a detailed technical explanation of why the person I was replying to was wrong. Does your browser not support websites others than Hacker News? What is this peculiar delusion you have in which content not written into this little box is not real content?

Hacker News comments are divided between useful, inquisitive engagement, to which I react positively and smarmy, self-important bullshit, to which I react with scorn. You have made clear your allegiance to the latter. Sad!


What are your expectations exactly when you try to patronize people and tell them about their "total ignorance" while also just throwing out links to random blog posts?

If someone replies with insults and then just links to a google search page you wouldn't think they're some sort of authority because they promised you the evidence to their claim is out there somewhere.

If you want people to actually respect what you're saying you need to do things differently from both angles. Stop the toxic claims of authority and give simple explanations that are direct replies to what people are talking about.


I created new accounts because I didn't register with an email and lost the login. What a nasty person you are! Why don't you go create something?


Some Rust people just have to be that dramatic.


I think “Pin” is a good example of a technically correct but hard to understand name. “Drop” has a much more familiar meaning because it’s a common action, whereas “pinning” isn’t so common and can mean different things in different contexts.

Rather than “pin!(…)”, would “immovable!(…)” be any better? Probably not, and it’s difficult to think of a better one.

So maybe a shorthand “colloquial” name doesn’t make sense, and something descriptive like “prevent_moving!(…)” and a PreventMove trait might be better?


It's a bad name for any application handling a Personal Identification Number of any kind.


Do people like titles like this where there is no information about what the link is (and what little is there has basically nothing to do with the link) ?

It seems like clickbait to me and the opposite purpose of having a site of headlines if they don't have any information.


> As it happens, this little corner of Rust is my mess

A nice surprise coming to this article with no context :)


Pinning/!Move is useful for so many things outside of async/await.

But because Rust fumbled it so badly, the usual answer is "rewrite your program in a language other than Rust."

(aside: there are about 4 different ways to implement object moving, and an efficient language needs to be aware of several of them)


> there are about 4 different ways to implement object moving

What are they?


0. no moves. This is very often needed for FFI callbacks, among others.

1. trivial copies. This is similar to 2, but means you do not have to do `swap`-like things in cases for non-destructive moves (which are, in fact, also important, they just shouldn't be the only kind of move. Note that supporting destructive moves likely implies supporting destructuring).

2. trivial moves. You can just use `memcpy`, `realloc`, etc. This is the only kind of move supported by Rust. C++ can introspect for it but with severe limitations. Note that "trivial" does NOT mean "cheap"; memory can be large.

3. moves with retroactive fixup (old address considered dead). What you do here is call `realloc` or whatever, then pass the old address (or the delta?) to a fixup function so that it (possibly with offset) can be replaced with the new address. Great caution is required to avoid UB by C's rules (the delta approach may be safer?). The compiler needs to be able to optimize this into the preceding when the fixup turns out to be empty (since generic wrapper classes may not know if their fields are trivially-movable or not).

4. full moves (both old and new address ranges are valid at the same time). C++ is the only language I know that supports this (though it is limited to non-destructive moves). One major use for this is maintaining "peer pointers" without forcing an extra allocation. Note that this can be simulated on top of "no moves" with some extra verbosity (C++98 anyone?).

Related to this, it really is essential for allocators to provide a "reallocate in place if possible, else fail" function, to avoid unnecessary move-constructor calls. Unfortunately, real-world allocators do not actually avoid copies if you use `malloc_usable_size` + `realloc`. If emitting C, note that you must avoid `__attribute__((malloc))` etc. to avoid setting off the bug-laden-piece-of-crap that is `__builtin_dynamic_object_size`.

Random reminder that "conditionally insert and move my object(s) into a container (usually a map), but keep my object alive if an equal one was already there" is important, and most languages do it pretty badly.

Linear types are related but it's all Blub to me.


[flagged]


It's not necessary for every single comment to address the entirety of the post.


seems rude to derail a topic by throwing one's offtopic ramblings into it WITHOUT READING IT


I take it Pin is a fixpoint-type that allows for recursive reference?


I think most people get a partial idea of what Pin is fairly quickly when they try, but that's part of the problem.

They struggle a lot more with Unpin, and particularly the dynamics as code changes. This comes up more often than in probably should in an async context. (re. "should", the problem is it shows up because of closure changes most often, and the errors (last I was teaching) take the user to the wrong place, typically the just need to allocate, but there's no practicum on their error understanding path, see below).

Users are integrating a change of some kind, get a compiler error and then struggle even further to really understand when and how the unpin marker exists, but a change, potentially at a fair distance from the code that they're editing, is now making compilation time demands that they both understand and address the issue.

When those users enter the argument with rustc, they come to a page that starts with:

  Implementing the Unpin trait for T expresses the fact that T is pinning-agnostic: it shall not expose nor rely on any pinning guarantees. This, in turn, means that a Pin-wrapped pointer to such a type can feature a fully unrestricted API. In other words, if T: Unpin, a value of type T will not be bound by the invariants which pinning otherwise offers, even when “pinned” by a Pin<Ptr> pointing at it. When a value of type T is pointed at by a Pin<Ptr>, Pin will not restrict access to the pointee value like it normally would, thus allowing the user to do anything that they normally could with a non-Pin-wrapped Ptr to that value.
Now this is reasonable documentation if you're in the thick of this universe, but to the average reader this is full of self-referential definition. When you have a weak understanding of Pin, then Unpin being defined in terms of Pin makes no sense, it's the same as Pin being defined in terms of Pin. All of this documentation needs to shift to a "systems engineering" or "in practice" basis first, and follow with theory. Most users give up with the docs before the second sentence they can't understand.

All of the cleverness (reliance on a priori theory) in the docs for these core types needs to be moved out of the way for most readers - replaced by much simpler terminology and practicum suitable for the average knuckledragger. Normal people who end up here in practice are on average already at saturation point for working memory, they have "no stack space left" for trying to unpack language theory.


I agree that this definition of Unpin should be improved, but I'm confused about the scenario you're describing. When does a user encounter an error because of Unpin?

Is it just that they use an API which requires Unpin? If so, what APIs do you encounter causing the issue most often?


Should add "rust" in the title, so that we know what the article is talking about.

If you're one of the "rust/go in title" haters... please go rage somewhere else.


This is literally the worst post title I've ever seen on HN :D


The corresponding rule is worded so harshly that I’m not surprised the poster didn’t dare to change it ( https://news.ycombinator.com/newsguidelines.html).


Not merely worded but also enforced that way.


That’s not true. The moderators accept suggestions and changes titles to something other than the original time and again. It is a guideline after all, it depends on context. I have zero doubts a more descriptive title would be accepted in this instance, as long as it were accurate and not clickbait.

https://hn.algolia.com/?query=by%3Adang%20change%20title&sor...


I've seen many cases of useful descriptive titles being switched to uninformative ones (Stephen Hawking's thesis is one that immediately comes to mind). Every title edit that I've seen in realtime has looked like a robotic replacement with the page title. (Perhaps whatever moderators are active in my timezone are less flexible about their guidelines than dang is)


At least it was a pleasant surprise that it’s not about the Humane AI Pin.


I'd already forgotten about that thing...!


> The Pin type (and the concept of pinning in general) is a foundational building block on which the rest of the the Rust async ecosystem stands.

Was this not the first sentence of the article when you wrote this?


It's about beeing able to quickly scan titles and decide if it's worth clicking on. If all titles were similar, we would spend a lot of time clicking each link to see if it is relevant.


It does indeed have nothing to do with boats.


But it also has everything to do withoutboats.


Why would anyone hate that? It’s a good marker, like [video], that tells folks like me not to bother clicking.


I've seen a decent amount of people that blacklist keywords like rust, blazingly, etc.


Which makes some sense if rust is only of tangential concern...

"I build cool thing X in rust applause" sucks and is borderline spammy imo

But when discussing a language feature, a type, including the language name should be a given and not trigger any rust haters


It’s against the rules for one


The “rules” are guidelines. Good titles should be useful and encourage those interested to read and do a service to those not interested by preemptively not wasting their time. Consider the following guidelines:

> Otherwise please use the original title, unless it is misleading or linkbait; don't editorialize.

> If you submit a video or pdf, please warn us by appending [video] or [pdf] to the title.

I don’t want a video so this is a good warning. “I wrote hello in rust” is not an article I want to read. “Type erasure in rust” could be an article I might click on.

The title “Pin” is so meaningless it’s like clickbait: someone might waste their time clicking on it and be annoyed, especially if they read synchronously (I just open a bunch of tabs in background in a single pass so it’s not as bad for me).

In this case I clicked the comments first to see what would happen if I were to click on the article. During the week I’d just skip it.

Yesterday there was a link to a tweet, so the title had to be made up. It could have been “Biden drops out” (people have lots of context) but was “Joe Biden stands down as Democratic candidate”. Just “Biden” would have been clickbait.


Even if you claim it's a guidelines, it's enforced like a rule


Not in my experience, but YMMV


I was disappointed not to see Intel Pin (https://www.intel.com/content/www/us/en/developer/articles/t...)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: