Live data from Hacker News

Why Rust closures are somewhat hard

stevedonovan.github.io

21–30 of 92 posts

Re: Why Rust closures are somewhat hard

#21

The fact that you can even do stuff like this in Rust is amazing. It’s simultaneously: relatively clean code, very efficient and type-safe. I like it.

Not sure I agree with "relatively clean code" when one of the examples show "fn compose (f1: impl Fn(T)->T, f2: impl Fn(T)->T) -> impl Fn(T)->T {" which is just a mix-match of keywords and other things, with tons of syntax embedded in just one line. But as always, depends on where you come from. I mostly deal with lisp languages nowadays, so guessing it's just my view that the line quoted above seems complex enough t…

That line can be simplified to:

  fn composeT>(f1: F, f2: F) -> F {

Re: Why Rust closures are somewhat hard

#22
post #8

I don't understand the intuition of closures and they turn me off to languages immediately. They feel like a hack from someone who didn't want to store a copy of a parent-scope variable within a function. The idea that I can touch variables that have gone out of scope (and that have ostensibly been GC'd) makes me feel that it is impossible to reason about variable lifetimes when dealing with closures. Is there some p…

> Is it literally just the language invisibly adding the parent-scope variables and their values to the top of my function?

I believe that would be 'lambda-lifting', i.e. taking a lambda which has free variables, adding those free variables to its input variables, moving the lambda out into the top- level, and modifying the call site to pass in the no-longer-free-variables. That should give you completely reasonable lifetimes and only require the stack.

It gets trickier when you want functions to be able to return functions. For instance:

  add(a, b) {
    a + b
  }

  myFun() {
    var add5 = x -> add(5, x);
    var add6 = x -> add(6, x);
    add5(1) + add6(2)
  }
In this case I've made two variants of add. I can't just run add5 on the stack, get rid of it, and then run add6 afterwards, because they both need to be alive at the same time.

Re: Why Rust closures are somewhat hard

#23

Earlier quoted context omitted.

Not sure I agree with "relatively clean code" when one of the examples show "fn compose (f1: impl Fn(T)->T, f2: impl Fn(T)->T) -> impl Fn(T)->T {" which is just a mix-match of keywords and other things, with tons of syntax embedded in just one line. But as always, depends on where you come from. I mostly deal with lisp languages nowadays, so guessing it's just my view that the line quoted above seems complex enough t…

That line can be simplified to: fn compose T>(f1: F, f2: F) -> F {

Lambdas have unique types, and you can't use a generic parameter in a return type (which is impl Trait's raison d'etre), so I think it would have to be this:

    fn compose T, G: Fn(T) -> T>(f: F, g: G) -> impl Fn(T) - > T
or:

    fn compose(f: F, g: G) -> impl Fn(T) -> T
      where F: Fn(T) -> T, G: Fn(T) -> T

Re: Why Rust closures are somewhat hard

#24

Earlier quoted context omitted.

Not sure I agree with "relatively clean code" when one of the examples show "fn compose (f1: impl Fn(T)->T, f2: impl Fn(T)->T) -> impl Fn(T)->T {" which is just a mix-match of keywords and other things, with tons of syntax embedded in just one line. But as always, depends on where you come from. I mostly deal with lisp languages nowadays, so guessing it's just my view that the line quoted above seems complex enough t…

That line can be simplified to: fn compose T>(f1: F, f2: F) -> F {

[deleted]

Re: Why Rust closures are somewhat hard

#25

Earlier quoted context omitted.

That line can be simplified to: fn compose T>(f1: F, f2: F) -> F {

Lambdas have unique types, and you can't use a generic parameter in a return type (which is impl Trait's raison d'etre), so I think it would have to be this: fn compose T, G: Fn(T) -> T>(f: F, g: G) -> impl Fn(T) - > T or: fn compose (f: F, g: G) -> impl Fn(T) -> T where F: Fn(T) -> T, G: Fn(T) -> T

Maybe you just miscommunicated, but you absolutely can use generic parameters in a return type:

https://doc.rust-lang.org/nightly/std/iter/trait.Iterator.ht...

... and `impl Trait` exists to be able to have return types which are difficult, verbose, or impossible to name.

Re: Why Rust closures are somewhat hard

#26
post #25

Earlier quoted context omitted.

Lambdas have unique types, and you can't use a generic parameter in a return type (which is impl Trait's raison d'etre), so I think it would have to be this: fn compose T, G: Fn(T) -> T>(f: F, g: G) -> impl Fn(T) - > T or: fn compose (f: F, g: G) -> impl Fn(T) -> T where F: Fn(T) -> T, G: Fn(T) -> T

Maybe you just miscommunicated, but you absolutely can use generic parameters in a return type: https://doc.rust-lang.org/nightly/std/iter/trait.Iterator.ht... ... and `impl Trait` exists to be able to have return types which are difficult, verbose, or impossible to name.

Yes, I think he meant that it would be a bad idea to lock all of the type parameters to the same type, because all closures have different types. So you wouldn't be able to do something like this

    compose(|x| x, |x| x)
because the closures have the same type.

Re: Why Rust closures are somewhat hard

#27
post #8

I don't understand the intuition of closures and they turn me off to languages immediately. They feel like a hack from someone who didn't want to store a copy of a parent-scope variable within a function. The idea that I can touch variables that have gone out of scope (and that have ostensibly been GC'd) makes me feel that it is impossible to reason about variable lifetimes when dealing with closures. Is there some p…

In JavaScript and similar languages, “yes.” It’s really a lot simpler than you’re making it. Closures are great in these languages and incredibly useful for rapid prototyping. Especially in Node.js and in browsers.

not just rapid prototyping. Often times, when i write unit tests and the like, i use closures. In the system i work in, asserting an exception requires spawning a new thread and then watching it burn, which is most easily accomplished with a closure. But also writing one-off tasks to be run in a new thread (even in prod) can take closures.

Re: Why Rust closures are somewhat hard

#28
post #8

I don't understand the intuition of closures and they turn me off to languages immediately. They feel like a hack from someone who didn't want to store a copy of a parent-scope variable within a function. The idea that I can touch variables that have gone out of scope (and that have ostensibly been GC'd) makes me feel that it is impossible to reason about variable lifetimes when dealing with closures. Is there some p…

> Is it literally just the language invisibly adding the parent-scope variables and their values to the top of my function?

Pretty much. When implementing closures, the variables that are going to be used by the nested functions are allocated on the heap instead of on the stack. The nested function is then set up to receive a pointer to the outer variables as an additional parameter.

If you want to read more about it, the word to search for would be "closure conversion"

http://matt.might.net/articles/closure-conversion/

It's unfortunate the the Wikipedia article for closure conversion now redirects to the article about Lambda Lifting. They are related but aren't quite the same thing.

Re: Why Rust closures are somewhat hard

#29
post #14

Earlier quoted context omitted.

Not sure I agree with "relatively clean code" when one of the examples show "fn compose (f1: impl Fn(T)->T, f2: impl Fn(T)->T) -> impl Fn(T)->T {" which is just a mix-match of keywords and other things, with tons of syntax embedded in just one line. But as always, depends on where you come from. I mostly deal with lisp languages nowadays, so guessing it's just my view that the line quoted above seems complex enough t…

Trait aliases will make some of those unwieldly declarations a bit easier on the eyes #![feature(trait_alias)] trait Map = Fn(T) -> T; fn compose (f1: impl Map , f2: impl Map ) -> impl Map { |n| f1(f2(n)) }

This is a nice feature, do you know any other language that has something similar?

Re: Why Rust closures are somewhat hard

#30
post #8

I don't understand the intuition of closures and they turn me off to languages immediately. They feel like a hack from someone who didn't want to store a copy of a parent-scope variable within a function. The idea that I can touch variables that have gone out of scope (and that have ostensibly been GC'd) makes me feel that it is impossible to reason about variable lifetimes when dealing with closures. Is there some p…

> The idea that I can touch variables that have gone out of scope (and that have ostensibly been GC'd)

I think this might be due to your mental model of GC not quite matching the way it's usually implemented (at least, in traditional GC, as opposed to reference counting); most GC gives no guarantees that memory will immediately be cleaned up once the last reference goes out of scope, just that it will happen some time in the future (usually the next time it checks). Languages like Rust (and C++, if you use destructors) will inline the memory cleanup at the end of the scope, but this is pretty explicitly not what will happen in a garbage-collected language.

Note also that this isn't really any different than the fact that a variable can go out of scope and not be cleaned up if another reference to it exists. Consider the following Java-esque code:

    void setNewChild(Parent parent) {
        var child = new Child();
        parent.child = child;
    } 
The `Child` created in the method body will not be garbage collected even though the variable goes out of scope because it's still referenced somewhere else (i.e. in the Parent object). The key insight to understanding closures is that using the local variable in another function makes another reference to it, so naturally it would be kept alive until that function is gone.
Post reply on HN