Hacker News new | ask | show | jobs
by marcosdumay 1227 days ago
There is nothing "natural" about working with Rust closures. If you want to take a losing fight against the borrow checker, they are the best way to do it.

(Rust has your C-like run of the mill function references too. People should emphasize those more, because differently from closures, those work very well.)

2 comments

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.

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

Wait, 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!
Indeed. 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));
    }
Your 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.
That’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.
I understand your point and perhaps I'm in fierce agreement.

OTOH, things like parser combinators are much more ergonomic in Haskell than Rust.

Well yes... For many reasons. Including automatic currying and the fact that eta reductions work seamlessly in Haskell and less so in Rust.
Closures in Rust are fundamentally broken. See [1] for the discussion.

[1] https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-...

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

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

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

That's not just closures, but a confluence of closures, async, and more.
The example provided in the post -- yes. But I can basically demonstrate other language features that are not working with closures, such as generics -- you just can't have generic closures in the same way as you have generic functions, even in a synchronous code. With closures, you have a pretty limited scope of what you can do, considering the rest of the language, so I think it'd be pretty useless to consider closures as a separate mechanism that shouldn't interact with the rest of the language.
> But I can basically demonstrate other language features that are not working with closures, such as generics -- you just can't have generic closures in the same way as you have generic functions, even in a synchronous code.

Generics and closures work just fine:

    use std::ops::Add;

    fn adder<T: Copy + Add<Output=T> + 'static>(x: T) -> impl Fn(T) -> Box<dyn Fn(T) -> T> {
        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));
    }
You keep making sensationalist generalizations. It's trivial to demonstrate that closures and generics work together just fine, as I've done above. Are there subtleties and complexities and other things that can make certain cases or areas tricky? Absolutely. But that's not the same as "not working."
Well, your example is not a generic closure, since inside `adder`, you already have a generic type `T`; it's introduced by the `fn` keyword. If we try to do the same for a closure, we would face a type error. This issue has been discussed on SO [1].

I haven't claimed that closures are not working, since well, they are working, but under a very limited set of circumstances. Again, I see nothing sensational, since the trickery of using closures has been discussed elsewhere.

[1] https://stackoverflow.com/questions/34814423/possible-to-def...

this comment made my day.