Live data from Hacker News

Why Rust closures are somewhat hard

stevedonovan.github.io

81–90 of 92 posts

Re: Why Rust closures are somewhat hard

#81

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…

While that may be an extreme example I agree. nothing clean about that code. I've only just begun to dive into rust, but I feel like I'd need a concordance to navigate the meaning of that snippet alone. Bt then again, maybe it depends on where you come from.

Takes a week or two of studying the language. It's well worth it, in my opinion.

Re: Why Rust closures are somewhat hard

#82
post #55

Earlier quoted context omitted.

This indictment makes little sense to me considering that, outside the Rust compiler itself, the largest early and still ongoing use case for Rust was/is Servo. A web browser engine, which is predominantly a thing that's dealing with multitudes of graph-shaped problems. :confused:

Have you looked at the issue tracker for Servo? The bugs and pain points are enlightening. Plus, Servo isn't even used in production! The strategy has been to move parts of it. AFAIU, the most complex piece so far has been the CSS engine. That's nothing to sneeze at, but still. I recently wrote a full PKIX X.509 DER decoder in LPeg in a couple nights of hacking; have fun doing that (or writing anything equivalent to…

The challenge with gc is that it itself isn’t a complete solution and actually creates new problems that are even harder to solve. Everyone worries about memory because it’s the most common resource but all resources have this problem (eg files, network handles, dB connections, audio handles, etc). So then you end right back to having GC languages add back scope-based ownership for that (python with, java closeable, etc). With RAII-style management like C++ or Rust all resources are treated uniformly.

Furthermore the challenge with concurrent GC is that max memory utilization is difficult to contain and difficult to make performance deterministic. So yes it has advantages. It also has severe disadvantages for the types of low-level efficient things Rust is meant for. Remember. Rust is meant to replace the existing problems that people reach for C/C++. That’s the space it’s competing in - embedded programming, operating systems, browsers, etc. There are problems that other languages are better suited for. Python, Ruby, Java are easier to work with not only because of GC. Java in particular pays an even larger cost due to not supporting value types and is still struggling afaik to integrate them properly into the GC which inhibits programmer-aided optimization of memory usage/layout.

Also, I’ll note that Swift and Objective-C are pretty easy to work with for memory management (on the level of Java) despite not having GC and get quite close to the performance of C/C++/Swift.

The challenges with GC that you handwave away has been what’s been said about GC ever since the beginning. Perhaps it’s time to admit that GC isn’t the silver bullet you think it is?

Re: Why Rust closures are somewhat hard

#83
post #77

Earlier quoted context omitted.

> It is effectively a slow ... raw pointer. In my experience (unless I've misunderstood what you're trying to say) this is the fastest way to write graphs because of how much more cache-friendly it is than having to chase a load of pointers.

True, but you can do the same thing more efficiently with pointers. Suppose the parent maintains an array of children Node* Then each child has a Node** member which points to itself in that array. That's one dereference to get up to the parent context. In the Rust example, to do the same would take an extra few steps and probably miss the cache. I think speculative execution would favor the pointer method.

Do you have benchmarks? An array offset isn’t speculative and I suspect you’ll have a very hard time showing that vec[i] is slower than *ptr. One challenge is if vec[i] involves a bounds check which it might in native rust.

Re: Why Rust closures are somewhat hard

#84
post #55

Earlier quoted context omitted.

Have you looked at the issue tracker for Servo? The bugs and pain points are enlightening. Plus, Servo isn't even used in production! The strategy has been to move parts of it. AFAIU, the most complex piece so far has been the CSS engine. That's nothing to sneeze at, but still. I recently wrote a full PKIX X.509 DER decoder in LPeg in a couple nights of hacking; have fun doing that (or writing anything equivalent to…

The challenge with gc is that it itself isn’t a complete solution and actually creates new problems that are even harder to solve. Everyone worries about memory because it’s the most common resource but all resources have this problem (eg files, network handles, dB connections, audio handles, etc). So then you end right back to having GC languages add back scope-based ownership for that (python with, java closeable,…

> Swift and Objective-C are pretty easy to work with

> and get quite close to the performance of C/C++/Swift.

In some cases, identical performance. ;)

Re: Why Rust closures are somewhat hard

#85
post #18
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 many ways, closures can be more difficult when there is not garbage collection because we precisely do not want to refer to values that have been freed or collected. For example, in C++, we can write code like: #include #include auto foo(double a) -> std::function { return [&a](auto x) { return x + a; }; } int main() { auto f = foo(2.0); std::cout This code is bad because the function returned by `foo` depends on…

In your rust example could you give "a" a lifetime that is the same as the lifetime of the impl Fn that foo() returns? That way "a" lives as long as the impl Fn.

Re: Why Rust closures are somewhat hard

#86
post #85
post #18

Earlier quoted context omitted.

In many ways, closures can be more difficult when there is not garbage collection because we precisely do not want to refer to values that have been freed or collected. For example, in C++, we can write code like: #include #include auto foo(double a) -> std::function { return [&a](auto x) { return x + a; }; } int main() { auto f = foo(2.0); std::cout This code is bad because the function returned by `foo` depends on…

In your rust example could you give "a" a lifetime that is the same as the lifetime of the impl Fn that foo() returns? That way "a" lives as long as the impl Fn.

I tried changing a to a reference and doing:

fn foo(a: &'a f64) -> impl Fn(f64) -> f64 + 'a { ....

but it doesn't work

Re: Why Rust closures are somewhat hard

#87
post #77

Earlier quoted context omitted.

True, but you can do the same thing more efficiently with pointers. Suppose the parent maintains an array of children Node* Then each child has a Node** member which points to itself in that array. That's one dereference to get up to the parent context. In the Rust example, to do the same would take an extra few steps and probably miss the cache. I think speculative execution would favor the pointer method.

Do you have benchmarks? An array offset isn’t speculative and I suspect you’ll have a very hard time showing that vec[i] is slower than *ptr. One challenge is if vec[i] involves a bounds check which it might in native rust.

I stand corrected. The contiguous vector is better and more cache-friendly.

  #include 
  #include 
  #include 
  
  static constexpr size_t MAX_NODES = 1 & descendant) {
    if (n index_self = n;
     descendant->ptr_self = &ptr_children[n];
     n += 1;
    }
   }
  };
  
  int main(void) {
   TopLevel top_level{};
   for (int i = 0; i ();
    top_level.add(node);
   }
   // access by pointer
   {
    auto t_0 = std::chrono::high_resolution_clock::now();
    Node** it = &top_level.ptr_children[0];
    for (int i = 0; i ptr_self;
     it += 1;
    }
    auto t_1 = std::chrono::high_resolution_clock::now();
    std::cout
        (
        t_1 - t_0)
        .count()
        index_self;
    for (int i = 0; i (
        t_1 - t_0)
        .count()
        
Results:

  % clang++ -O3 bench.cc && ./a.out
  Via pointer: 90 ns
  Via index: 41 ns

Re: Why Rust closures are somewhat hard

#88
post #30

Earlier quoted context omitted.

> 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 tim…

> most GC gives no guarantees that memory will immediately be cleaned up once the last reference goes out of scope But in this example the last reference hasn't gone out of scope, the last reference is in the closure. If the GC ignored closures and acted as if last reference had gone out of scope then there's a chance that that variable would be cleaned up before the closure gets called, which would make closures com…

I think we're saying the same thing! The point I was trying to make with the second half of my comment was that a variable going out of scope isn't the same thing as the object going out scope, since other references can still be alive afterwards. The example I gave was intended to demonstrate this without using closures (since the person I was responding to had indicated that they found them confusing), and then went on to attempt to explain that the example with just objects was in fact doing the same thing as if the object were referenced in a closure rather than another object. The first half of my comment was just an attempt to respond to the statement about the object having "ostensibly been GC'd", which isn't necessarily the case even if there wasn't a closure referencing the object; in your phrasing, there's a chance that it would be cleaned up, and I wanted to make sure that it was clear that it was only a chance, not a guarantee.

Re: Why Rust closures are somewhat hard

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

I think Gluon is a good example of how Rust can be 'bulked up'.

The idea is to have a DSL inside Rust that essentially has haskell semantics. This allows you to write performance sensitive code in Rust and then marshall it into Gluon where you can do high level manipulations with type safety before you unmarshall into a performance sensitive section again.

To me the failure of Rust is more that it didn't go full-on-haskell syntax wise and (to a lesser extent) that there isn't a specification (or some standards) beyond the test suite.

Re: Why Rust closures are somewhat hard

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

The reason it's amazing is that it's being done without a GC, while still giving you control over allocation, and even being able to inline the lambdas in many places.

It's not that lambdas themselves are hard, it's that they made it work with all of the other constraints of the language.

Post reply on HN