4 ms·
Closures work very well in Rust and are a core aspect of the standard library. The 'Iterator' trait adapters, for example, make very heavy use of closures, as d
by burntsushi 4y ago
Closures work very well in Rust and are a core aspect of the standard library. The 'Iterator' trait adapters, for example, make very heavy use of closures, as do the combinators on 'Option' and 'Result'. Closures are also how you spawn threads. They are absolutely ubiquitous and quite natural.
In contrast, function pointers are very rarely used.
Are they equivalently nice in every way to closures in Haskell? Of course not. But I think your comment is swinging way too far in the opposite direction.
- chongli 4y agoI think the word ‘natural’ in this context is too vague. If you’re coming to Rust from C++, sure, Rust’s closures feel quite natural. If, on the other hand, you’re coming to Rust from Haskell then they’ll feel wholly unnatural and confusing.
- burntsushi 4y agoSure it's vague. We're expressing opinions here. :-) But it's important to push back here. Closures are absolutely natural enough in Rust that a huge swath of very fundamental APIs in Rust rely on them. If they weren't natural in at least some sense, it would be absolutely bonkers to do that. I came to Rust from Haskell (among other languages). I was a little confused by closures, but I attributed that to the fact that I started writing Rust before 1.0 when closures were far far far less convenient than they are today. (This is back when there were 'proc' closures.) Now I find them extremely natural.
- chongli 4y agoWait, they got rid of proc closures? Hahaha. Shows how long it’s been since I last looked at Rust. Okay, now they do look quite natural!
- burntsushi 4y agoIndeed. My (and by no means am I the only one) mental model of closures is really simple. A closure is a struct with no name, and the free variables of the closure are fields in the struct. If the closure is annotated with 'move', then those fields are owned, otherwise the fields are borrowed and the lifetime parameter on the struct is the shortest of the bunch. Then, calling the closure is just calling it with 'self' (FnOnce), '&self' (Fn) or '&mut self' (FnMut). Everything else pretty much falls into place. The other thing that was added since you've looked at Rust is that you can return unboxed closures because Rust has gained the ability to express anonymous types directly by naming a trait it implements: fn adder(x: i32) -> impl Fn(i32) -> i32 { move |y| y + x } fn main() { let add2 = adder(2); assert_eq!(7, add2(5)); assert_eq!(10, add2(8)); }
- chongli 4y agoThat’s really nice. One of my favourite uses for returning closures is to separate a function into ‘configuration’ and ‘execution’ stages. It becomes extremely useful when you have a ton of parameters but only a few change between typical invocations.
- Hirrolot 4y agoYour example indeed perfectly demonstrates the subtlety of closures in Rust. Now let's imagine I want to return a nested closure, e.g., `impl Fn(i32) -> impl Fn(i32) -> i32`, or something like that: fn adder(x: i32) -> impl Fn(i32) -> impl Fn(i32) -> i32 { move |y| move |z| y + x + z } Alright, I can't. I can do that for a single closure, but can't for two or more of them. Here's why we have `#![feature(impl_trait_in_fn_trait_return)]` -- in Nightly, among many other such features.
- burntsushi 4y agoMy example didn't demonstrate the subtlety here, but it inspired your example, which of course does demonstrate some subtlety. And as I acknowledged in another comment, I of course agree there are subtleties to closures in Rust! And I agree there are useful nightly features that unlock additional use cases. Rust gives you enough rope to hang yourself with. For example, if you're okay with a heap allocation (like I assume you might be in a language with "non-broken closures" lol), then you don't need nightly Rust: fn adder(x: i32) -> impl Fn(i32) -> Box<dyn Fn(i32) -> i32> { move |y| Box::new(move |z| y + x + z) } fn main() { let add23 = adder(2)(3); assert_eq!(10, add23(5)); assert_eq!(13, add23(8)); } It's almost like "fundamentally broken" (not just "broken" but "fundamentally broken") and "has subtlety" are two totally different things. What a fucking revelation.
- Hirrolot 4y agoClosures in Rust are fundamentally broken. See [1] for the discussion. [1] https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-of-mainstream-programming.html https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-...
- burntsushi 4y agoLol. "fundamentally broken" and yet I've been using them with little to no friction for pretty close to a decade now. If that's what you think "broken" means, then, well, you've completely devalued the word. This right here is what we call sensationalism folks. Now if you said, "Rust has some rough points at the intersection of generics, closures and async programming," I'd say that's absolutely true!
- Hirrolot 4y ago> "fundamentally broken" and yet I've been using them with little to no friction for pretty close to a decade now. This might rather confirm my point, since engineers using a specific programming language quite often have a contorted picture of how code in other languages is written, as they become more and more acquainted with their main tool. If we compare Rust closures with those of ML languages, it becomes pretty clear how natural closures are in ML and tricky in Rust. Not quite a sensation either to anybody who happened to use closures in a typical Rust code, which, apparently, happens to have quite a lot of generics and async!
- burntsushi 4y agoThe sneer is strong with this one. > This might rather confirm my point It might, but it doesn't, because I don't only use Rust. Notice also how you've moved the goalposts from "fundamentally broken" (sensationalist drivel) to "how natural closures are in ML and tricky in Rust" (a not unreasonable opinion with lots of room to disagree on the degree of "natural" and "tricky").
- pitaj 4y agoThat's not just closures, but a confluence of closures, async, and more.
- SnowflakeOnIce 4y agoI understand your point and perhaps I'm in fierce agreement. OTOH, things like parser combinators are much more ergonomic in Haskell than Rust.
- burntsushi 4y agoWell yes... For many reasons. Including automatic currying and the fact that eta reductions work seamlessly in Haskell and less so in Rust.