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.
A guide to closures in Rust
71–80 of 102 posts
Re: A guide to closures in Rust
#72> 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…
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
#73There'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.
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
#74Earlier 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.
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
#75Re: A guide to closures in Rust
#76Re: A guide to closures in Rust
#77Earlier 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.
Re: A guide to closures in Rust
#78There'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 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> 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.
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.
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.