Live data from Hacker News

A guide to closures in Rust

hashrust.com

71–80 of 102 posts

Re: A guide to closures in Rust

#71

Earlier quoted context omitted.

Closing over variables is the thing that makes it a closure. Otherwise, you just have an anonymous function. A closure is a function plus the captured environment. The difference is meaningful here. You have to allocate a closure (and deal with its lifetime and the lifetimes of the variables it references) but the anonymous function is just a pointer to static code in the binary. That's the entire difficulty with clo…

> You have to allocate a closure That's incorrect, a closure in Rust compiles down to a static function that takes its environment as an argument. None of that requires a heap allocation in the above code.

Stack allocation is still allocation.

Re: A guide to closures in Rust

#72
post #43

> let add_closure = |a, b| a + b; let sixty_six = add_closure(42, 24); Woah woah, how did it not occur to me that I could just…construct a closure that directly in Rust? This feels like one of those “incredibly obvious and reasonable in hindsight” things. I don’t know why I was labouring under the assumption they could only be invoked in very special and specific places, but it’s good to know I was wrong.

If you want to do anything with the closure it gets a little tricker, for example if you did something naive like fn fn_that_takes_closure (f: fn (i32, 3i2) -> i32) { ... } let x = |a, b| a + b; fn_that_takes_closure(x); That would not compile, because `x` does not have the type `fn (i32, i32) -> i32` (that's reserved for "normal" functions). Every closure in Rust has a unique anonymous type, which means if you want…

> that's reserved for "normal" functions

Function pointers. Rust's functions all have unique anonymous types too.

In C or C++ the functions isupper and islower (which are predicates that decide whether a char is an upper or lowercase letter respectively) have the same type.

In Rust such functions always have their own unique anonymous type. char::is_uppercase and char::is_lowercase don't have the same type.

This has a consequence when we want to use a functional-like specialisation. For example "Walter White".starts_with(char::is_uppercase) isn't just passing a function pointer - that can't work, what happens instead is there's an implementation of Pattern on things which match a certain FnMut trait, these two function types match that trait, therefore Pattern is implemented for each type, so there's a monomorphization step, baking a custom implementation of this function for this specific predicate, if you use a different predicate you get a different monomorphization.

Re: A guide to closures in Rust

#73
post #23

There's a lot to be said in favor of Rust's approach to memory management but closures in Rust suck compared to garbage collected languages.

That's because Rust wants you to use the stack almost exclusively for memory allocation because that's the obvious way to do automatic memory management w/o a GC. So the moment you want to return closures you have to allocate them on the heap (Box them).

Rust is a modern, functional programming language where the programmer works in a straightjacket, and this is most painfully evident the moment you want to use closures like you're programming in Haskell.

In Haskell you don't have to think about stack vs. heap allocation. But w/o a way to make it easier for the compiler to choose stack allocation where that would be safe, Haskell ends up being heap-heavy. Rust takes the opposite tack and is stack-heavy, but unlike Haskell Rust forces you to be explicit about the alternative.

It's not like Rust doesn't have a GC. Arc is a GC after all. It's just that Rust makes it hard to write natural code using the heap and GC.

Re: A guide to closures in Rust

#74
post #42

Earlier quoted context omitted.

The borrow checker rejects many otherwise valid programs. So if you do your job in correctly in C/C++, the borrow checker might accept the Rust equivalent or it might not.

Maybe that was true in 2017, but today borrow checker and compiler in general has covered so much of the Rust design space that the "correct programs" it rejects are more like rejecting Duff's Device kind of code. You are more likely to find yourself implementing low-level data structure or device interface where you need raw pointers tightly localized in the unsafe{} scope.

The article says that this code doesn't compile in 2023. Assuming the intention was to print "fox", I don't see how it is incorrect:

  let mut animal = "fox".to_string();
  let mut capture_animal = || {
      animal.push_str("es");
  };
  //ERROR:cannot borrow `animal` as immutable because it is also borrowed as mutable
  println!("Outside closure: {animal}");
  capture_animal();

Re: A guide to closures in Rust

#77

Earlier quoted context omitted.

True, but isn't the complexity of Rust closures essential rather than accidental? Fundamentally closures are easy in e.g. Go because you don't have to think about lifetimes, at all. As soon as you capture a variable, the GC guarantees it won't be dropped from underneath your feet. With non-GC'd languages that responsibility moves from the GC to the programmer. The trickyness of using closures in Rust seems largely to…

Not quite, there are non-GC ways to guarantee memory safety without borrow checking, see languages like Vale, HVM (more a runtime), and Inko. With those in mind, Rust's complexity does indeed look accidental here. It has its other benefits, but it does make closures a bit more difficult.

Inko uses automatic reference counting, which you can argue about definitions, but I would consider to be GC. At any rate, it's not relevant to whether Rust's complexity is accidental or not, because Rust specifically doesn't do automatic reference counting, and instead uses the borrow checker at compile time.

Re: A guide to closures in Rust

#78
post #23

There's a lot to be said in favor of Rust's approach to memory management but closures in Rust suck compared to garbage collected languages.

For a long time, people thought closures were only practical in garbage collected languages. I've used closures in C. (Against my will, basically. It was a C library that was best-of-breed and there was no other choice.) Getting the memory management right on them was effectively impossible. You know where the values are created for sure, but creating clean specifications of when the closures were destroyed quickly becomes insane in any real program. Very simple in a 20-line sample, very complicated when you got a long-lived callback interacting with multiple other resources that may or may not have their own "interesting" lifespans.

That Rust can do closures at all, do them in a sufficiently useful way that they are practical, and still maintain its safety guarantees without garbage collection is, in my opinion, in the top five accomplishments of the language. Prior to it actually happening I think it's fair to say most programming language developers would have said it's not possible, that you'll either need GC or the requisite type system will be impractically complex. At least their quirkiness generally fits the rest of the language and isn't much "new" quirkiness.

Re: A guide to closures in Rust

#79
post #2

> I'll explain what the move keyword does later in the article. For now just trust me that it is needed for the code to compile. That does not take a lot of trust, after a few rounds with that compiler. At least error messages are good with the Rust compiler. I've got very limited experience with Rust, but it does seem like a language with a massive threshold for beginners.

I know I keep repeating this, but...

Rust does not have a massive threshold. Memory management has a massive threshold. Rust just has a lot of safety rails you might keep bumping into if you struggle with memory management. The fact that other languages let you drive off the cliff doesn't mean they have a smaller threshold.

Re: A guide to closures in Rust

#80

> let add_closure = |a, b| a + b; let sixty_six = add_closure(42, 24); Woah woah, how did it not occur to me that I could just…construct a closure that directly in Rust? This feels like one of those “incredibly obvious and reasonable in hindsight” things. I don’t know why I was labouring under the assumption they could only be invoked in very special and specific places, but it’s good to know I was wrong.

It becomes more painful when the closure is actually closing over something (especially if that thing is mutable), since the borrow checker gets involved.

This is a key thing to know, actually.

Closures in Rust are different from named functions and that can trip you up.

Closures in Rust also close over lifetime information in ways that named functions cannot do. This confused me for quite a while.

Post reply on HN