Live data from Hacker News

Why Rust closures are somewhat hard

stevedonovan.github.io

41–50 of 92 posts

Re: Why Rust closures are somewhat hard

#41
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…

I know you've already got a storm of replies, but I'd like to illustrate part of what I love about closure (especially when combined with anonymous objects like in JavaScript): you can replace classes, private vs public, "this", and other features with a simpler, smaller number of features.

    const Counter = () => {
      let counts = new Map();

      return {
        reset: () => {
          counts = new Map();
        },

        count: (key) => {
          const count = counts.get(key) + 1;
          counts.set(key, count);
          return count;
        }
      }
    }

    const items = ['apple', 'apple', 'banana', 'canteloupe'];
    const counter = Counter()

    items.forEach(item => {
      console.log(item + ": count is", counter.count(item));
    })

    counter.reset();
    // do some more counting now
IMO, this is much simpler [and prettier ;)] than the alternative using classes: you only need to know closures and objects, and the rules apply the same as they do in all other contexts. Classes in most languages usually come with their own twists and surprises.

Re: Why Rust closures are somewhat hard

#42
post #39

It feels like the part that's missing is the ability to abstract over these different types of function, polymorphically. At least, that seems to be where things fall down when we try to talk about e.g. implementing a Functor trait in Rust. As a concrete example, the `compose` method given should clearly be the same for `Fn`, `FnOnce`, and `FnMut`. Can we write it once and reuse it for `Fn`, `FnOnce` and `FnMut`? If…

Because Rust doesn't have higher-kinded types. Well it does but it doesn't allow you to define or implement traits for them.

Also, what happens when you compose one function with FnMut and another with just Fn? The answer is clearly FnMut. These three traits are related by a subtyping relationship. I don't know enough Rust to answer the question of whether such a hypothetical compose function can return the right one from this hierarchy.

Re: Why Rust closures are somewhat hard

#44
post #35

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.

> The fact that you can even do stuff like this in Rust is amazing. Stuff like this? You mean closures? That's what you find amazing? What is considered ordinary in other programming languages is considered amazing in Rust. Amazing. > It’s simultaneously: relatively clean code, very efficient and type-safe. I like it. You like it? Do you work for mozilla? If there is a tech evangelist of the year award, I will vote f…

This comment breaks quite a whole lot of the site guidelines. Please don't post in the flamewar style to HN.

If you'd please review https://news.ycombinator.com/newsguidelines.html and take the intended spirit of this place more to heart, we'd be grateful.

Re: Why Rust closures are somewhat hard

#45

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…

    fn compose(
        f1: impl Fn(T)->T,
        f2: impl Fn(T)->T
    ) -> impl Fn(T)->T
    {
        // both arguments as well as the returned value 
        // are functions which take and return the generic type T.
        // (to be precise, they implement the Fn trait).
    }
    
    let returned_function = compose(|val| val == 1, |val| val 

Re: Why Rust closures are somewhat hard

#48
Everything that's hard in Rust could be solved by GC.

With Rust, programmers have to spend most of their mental energy worrying about management of memory, which has largely been automated already.

I always go back to this quote from Andrei Alexandrescu (creator of D):

  A disharmonic personality. Reading any amount of Rust code evokes the
  joke "friends don't let friends skip leg day" and the comic imagery of
  men with hulky torsos resting on skinny legs. Rust puts safe, precise
  memory management front and center of everything. Unfortunately,
  that's seldom the problem domain, which means a large fraction of the
  thinking and coding are dedicated to essentially a clerical job (which
  GC languages actually automate out of sight). Safe, deterministic
  memory reclamation is a hard problem, but is not the only problem or
  even the most important problem in a program. Therefore Rust ends up
  expending a disproportionately large language design real estate on
  this one matter. It will be interesting to see how Rust starts bulking
  up other aspects of the language; the only solution is to grow the
  language, but then the question remains whether abstraction can help
  the pesky necessity to deal with resources at all levels.
RAII and borrow checking is a crutch. You just limited the set of programs you can write to those that can be written in the block-lifetime-scoped manner, which is smaller than the set of good programs (see: pretty much any graph-heavy data structures). The limitations of this programming model show up everywhere, like in the way closures have to be implemented.

There will be more innovation in GC that will make manual memory management even more useless. In a lot of cases the JVM does a better job of freeing memory than a programmer. I don't want to spend my time programming worrying about the same thing (memory) that K&R did in the 70s. I don't want to bet against innovation and technology.

Re: Why Rust closures are somewhat hard

#49
post #48

Everything that's hard in Rust could be solved by GC. With Rust, programmers have to spend most of their mental energy worrying about management of memory, which has largely been automated already. I always go back to this quote from Andrei Alexandrescu (creator of D): A disharmonic personality. Reading any amount of Rust code evokes the joke "friends don't let friends skip leg day" and the comic imagery of men with…

> You just limited the set of programs you can write to those that can be written in the block-lifetime-scoped manner, which is smaller than the set of good programs (see: pretty much any graph-heavy data structures).

Given that most of my Rust programs have involved graph-heavy data structures, I'd like to see you explain why it's impossible for me to have written what I have written.

Re: Why Rust closures are somewhat hard

#50
post #31

There's a lot of confusion about this. A lambda is just a function without a name. (This feature tends to come with special syntax, although it doesn't have to.) A nested function is a function defined inside another function which can access the variables of the enclosing function. (A nested function can be a lambda, but it doesn't have to be. Some languages have named nested functions. A lambda doesn't have to be a…

I think the OP makes it clear that this nuance is actually quite tricky in a native systems language like Rust.

A good example in C++ would be:

- A function (non capturing/lambda) is created with

  auto f = [] { ... };
- A function (capturing/closure) is created with

  auto s = "...";
  auto f = [&] { s; ... };
In source code both of those look similar and you can even define them with the same type: std::function

However, the resulting assembly for both couldn't be more different. The Lambda case is as raw as any normal C function type, which the closure case creates a C++ class that closes over the outer method's state. If you're not a seasoned C++ engineer, this nuance will be lost on you.

It's fair to say that everyone should understand Lambda Calculus rules, but that makes the language less accessible to new-comers. In the case for non-systems languages, you can blur the lines with ease, but something Rust and C++ just cannot afford to do. That makes understanding the nuance important to be effective.

Post reply on HN