Hacker News new | ask | show | jobs
by FridgeSeal 1119 days ago
> 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.

3 comments

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 to do anything with them besides calling them, you need to understand `Fn`, `FnOnce`, and `FnMut` traits and/or `dyn` trait objects. To write that above you'd need to do something like this:

    fn fn_that_takes_closure<F> (f: F)
    where
        F: Fn(i32, i32) -> i32
> That would not compile

It does compile.

> because `x` does not have the type `fn (i32, i32) -> i32` (that's reserved for "normal" functions).

Closures can cast to function pointers just fine as long as they don't capture anything.

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 closures in non-GC languages.

This distinction matters less in GC languages where you're not thinking about lifetimes either way.

As per Rust reference, even a capture-less closure is a closure and distinct from an anonymous functions.

Also, your arguments only partly apply in Rust. Rust doesn't heap-allocate closures. And you also often don't have to deal with lifetimes - a closure that captures variables by move or copy is perfectly self-contained

The difference between a closure that captures and a closure that doesn't is like the difference between `(T)` and `()` - same kind of thing, so it adheres to the same terms and behaviors

> 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.
Literally no considers that to be the case? Like I have never met anyone who would consider declaring an integer variable "allocation"
I didn't suggest a heap allocation; indeed, in C++ too you can have stack allocated closures. I mean that it's not simply a pointer to static code; there is an associated data structure.
Other comments are saying non-capturing closures are not closures. If we want to have this pedantic argument of definitions: proto_lambda is actually right, with Rust's definition of closures.

According to the Rust reference:

> Closure types > > A closure expression produces a closure value with a unique, anonymous type that cannot be written out.

https://doc.rust-lang.org/reference/types/closure.html

Even a non-capturing closure is written using a closure expression and produces a unique, anonymous type.

Isn't the distinguishing feature of a closure that it can capture stuff from the environment?

> Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

> The term closure is often used as a synonym for anonymous function, though strictly, an anonymous function is a function literal without a name, while a closure is an instance of a function, a value, whose non-local variables have been bound either to values or to storage locations (depending on the language; see the lexical environment section below).

My understanding is that your use of 'capture' matches up with the the use of 'bound' in the description above.

https://en.wikipedia.org/wiki/Closure_(computer_programming)

It can capture things, OP's example does not capture anything.
people casually using the term closure for anonymous functions has caused so much confusion over the years lol
If it doesn't capture anything, then it's not a closure.
There’s so much confusion out there due to people throwing out the term “closure” imprecisely, ugh
> 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.

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.

Welcome to functional programming, where functions are values.