NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
Understanding Rust Closures (antoine.vandecreme.net)
BlackFly 1 hours ago [-]
The biggest friction I experience with respect to rust closures is their inability to be generic: I cannot implement a method that takes a closure generic over its argument(s).

So then I'm forced to define a trait for the function, define a struct (the closure) to store the references I want to close over, choose the mutability and lifetimes, instantiate it manually and pass that. Then the implementation of the method (that may only be a few lines) is not located inline so readability may suffer.

wtetzner 26 minutes ago [-]
Do you have an example of this? I'm not sure I follow it exactly.
andy_xor_andrew 12 hours ago [-]
if I'm not mistaken (and I very well may be!) my primary confusion with closures comes from the fact that: the trait they implement (FnOnce / Fn / FnMut) depends entirely upon what happens inside the closure.

It will automatically implement the most general, relaxed version (FnMut I think?) and only restrict itself further to FnOnce and Fn based on what you do inside the closure.

So, it can be tricky to know what's going on, and making a code change can change the contract of the closure and therefore where and how it can be used.

(I invite rust experts to correct me if any of the above is mistaken - I always forget the order of precedence for FnOnce/Fn/FnMut and which implies which)

bobbylarrybobby 7 hours ago [-]
The three Fn* types correspond to the three ways you can refer to a value: &T, &mut T, T. Fn captures its environment by shared reference, FnMut by exclusive reference, and FnOnce by value, and everything flows from that. Calling a Fn is the same as using a reference. Calling a FnMut is the same as using a mutable reference (you can do it as many times as you want but no two uses may overlap in time). And calling a FnOnce is the same as moving a value (you can do it at most once).
yoshuaw 10 hours ago [-]
> I always forget the order of precedence for FnOnce/Fn/FnMut

The way I remember the ordering is by thinking about the restrictions the various Fn traits provide from a caller's perspective:

  1. FnOnce can only ever be called once and cannot be called concurrently. This is the most restrictive.

  2. FnMut can be called multiple times but cannot be called concurrently. This is less restrictive than FnOnce.

  3. Fn can be called multiple times and can even be called concurrently. This is the least restrictive.
So going from most to least restrictive gives you `FnMut: FnOnce` and `Fn: FnMut`.
umanwizard 10 hours ago [-]
Fn can only be called concurrently if its environment is Sync, which is often true but not necessarily.

It’s more precise to say that Fn can be called even when you only have shared access to it, which is a necessary, but not sufficient, condition for being able to be called concurrently.

7 hours ago [-]
csomar 2 hours ago [-]
My issue is that there is no easy way to know the signature generated by the closure (unlike a function) until you read the compiler errors and even then it's some cryptic mess because closures are anonymous structs. Or maybe I missed some LSP config/extension?
umanwizard 10 hours ago [-]
The least restrictive for the caller is Fn (you can call it whenever), then FnMut (you can call it only if you have exclusive access to it, as many times as you want), then FnOnce (you can call it only if you have exclusive owned access, and calling it once destroys it).

The least restrictive for the function itself is the opposite order: FnOnce (it can do anything to its environment, including possibly consuming things without putting them back into a consistent state), followed by FnMut (it has exclusive access to its environment, and so is allowed to mutate it, but not destroy it), followed by Fn (it has only shared access to its environment and therefore is not allowed to mutate it).

Since these orders are inverses of each other, functions that are easier to write are harder to call and vice versa. That’s why they implement the trait with the minimum amount of power possible, so that they can be called in more places.

KolmogorovComp 10 hours ago [-]
This is correct. But it’s not really surprising, it’s type inference.
gpm 8 hours ago [-]
It isn't really type inference. Each closure gets a unique type. Rather it's an automatic decision of what traits (think roughly "superclasses" I guess if you aren't familiar with traits/typeclasses) to implement for that type.
chowells 3 hours ago [-]
So you're saying... it's type inference of type classes, just like in Haskell?
csomar 2 hours ago [-]
I am not sure how Haskell works but I think what the previous poster meant is that the types get determined at compile time. Closures are akin to macros except you can't see the expanded code.
krukah 9 hours ago [-]
Easiest mnemonic to remember precedence is simply ordering by the length of their names.

FnOnce

FnMut

Fn

OptionOfT 4 hours ago [-]
I wish there was more customizability with regards to captures.

the move keywords captures everything. Sometimes I want a little bit more flexibility, like C++ lambdas.

avandecreme 2 hours ago [-]
This article discusses making captures more flexible: https://smallcultfollowing.com/babysteps/blog/2025/10/22/exp...

I agree it would be nice, in particular to make it easier to understand when learning the concept.

7 hours ago [-]
Sytten 8 hours ago [-]
And now we have the Async version of each of those, which makes me very happy and reduces the shenanigans and lifetime issues.

Also worthy of note is that there is talk to add a syntax for explicit captures.

amelius 12 hours ago [-]
Closures are the bread and butter of functional programming, but Rust made closures a complicated mess.
openuntil3am 11 hours ago [-]
Closures are a complicated mess. Functional programming languages hide the mess with garbage collection.
andrewflnr 2 hours ago [-]
This isn't the right framing IMO. Closures actually aren't complicated with GC for the same reason structs with references aren't complicated, at least as far as the programmer is concerned. You could say functional languages "hide the mess" there too, but even if you take that perspective, it's nothing to do with closures in particular. Closures are just one of the things that need memory, and memory management is tricky without GC.
Klonoar 12 hours ago [-]
If you understand the borrow checker, closures are just not that much on top of things.

In fact I can’t remember the last time I had to fight with them.

convolvatron 11 hours ago [-]
I really wanted just yesterday to create a dyn AsyncFnMut, which apparently still needs async-trait to build the stable. but I was pretty much unable to figure out how to make that work with a lambda. saying this is all trivial once you understand the borrow machinery is really understating it.
kibwen 10 hours ago [-]
> saying this is all trivial

The comment above isn't saying that closures are trivial. Once you understand the borrow checker, you understand that it's a miracle that closures in Rust can possibly work at all, given Rust's other dueling goals of being a GC-less language with guaranteed memory safety despite letting closures close over arbitrary references. Rust is in uncharted territory here, drawing the map as it goes.

speed_spread 9 hours ago [-]
Async is the stuff that messes up everything. Closures are not complicated.
ordu 10 hours ago [-]
Well... Rust is not a functional language, so it is not surprising that its closures are complicated.
Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 10:14:25 GMT+0000 (Coordinated Universal Time) with Vercel.