37 pointsby avandecreme7 hours ago2 comments
  • andy_xor_andrew3 hours ago
    if I'm not mistaken (and I very well may be!) my primary confusion with closures comes from the fact that: the trait they implement (FnOnce / Fn / FnMut) depends entirely upon what happens inside the closure.

    It will automatically implement the most general, relaxed version (FnMut I think?) and only restrict itself further to FnOnce and Fn based on what you do inside the closure.

    So, it can be tricky to know what's going on, and making a code change can change the contract of the closure and therefore where and how it can be used.

    (I invite rust experts to correct me if any of the above is mistaken - I always forget the order of precedence for FnOnce/Fn/FnMut and which implies which)

    • yoshuawan hour ago
      > I always forget the order of precedence for FnOnce/Fn/FnMut

      The way I remember the ordering is by thinking about the restrictions the various Fn traits provide from a caller's perspective:

        1. FnOnce can only ever be called once and cannot be called concurrently. This is the most restrictive.
      
        2. FnMut can be called multiple times but cannot be called concurrently. This is less restrictive than FnOnce.
      
        3. Fn can be called multiple times and can even be called concurrently. This is the least restrictive.
      
      So going from most to least restrictive gives you `FnMut: FnOnce` and `Fn: FnMut`.
      • umanwizardan hour ago
        Fn can only be called concurrently if its environment is Sync, which is often true but not necessarily.

        It’s more precise to say that Fn can be called even when you only have shared access to it, which is a necessary, but not sufficient, condition for being able to be called concurrently.

    • krukah27 minutes ago
      Easiest mnemonic to remember precedence is simply ordering by the length of their names.

      FnOnce

      FnMut

      Fn

    • KolmogorovComp2 hours ago
      This is correct. But it’s not really surprising, it’s type inference.
    • umanwizard2 hours ago
      The least restrictive for the caller is Fn (you can call it whenever), then FnMut (you can call it only if you have exclusive access to it, as many times as you want), then FnOnce (you can call it only if you have exclusive owned access, and calling it once destroys it).

      The least restrictive for the function itself is the opposite order: FnOnce (it can do anything to its environment, including possibly consuming things without putting them back into a consistent state), followed by FnMut (it has exclusive access to its environment, and so is allowed to mutate it, but not destroy it), followed by Fn (it has only shared access to its environment and therefore is not allowed to mutate it).

      Since these orders are inverses of each other, functions that are easier to write are harder to call and vice versa. That’s why they implement the trait with the minimum amount of power possible, so that they can be called in more places.

  • amelius4 hours ago
    Closures are the bread and butter of functional programming, but Rust made closures a complicated mess.
    • openuntil3am3 hours ago
      Closures are a complicated mess. Functional programming languages hide the mess with garbage collection.
    • Klonoar3 hours ago
      If you understand the borrow checker, closures are just not that much on top of things.

      In fact I can’t remember the last time I had to fight with them.

      • convolvatron2 hours ago
        I really wanted just yesterday to create a dyn AsyncFnMut, which apparently still needs async-trait to build the stable. but I was pretty much unable to figure out how to make that work with a lambda. saying this is all trivial once you understand the borrow machinery is really understating it.
        • kibwen2 hours ago
          > saying this is all trivial

          The comment above isn't saying that closures are trivial. Once you understand the borrow checker, you understand that it's a miracle that closures in Rust can possibly work at all, given Rust's other dueling goals of being a GC-less language with guaranteed memory safety despite letting closures close over arbitrary references. Rust is in uncharted territory here, drawing the map as it goes.

        • speed_spreadan hour ago
          Async is the stuff that messes up everything. Closures are not complicated.
    • ordu2 hours ago
      Well... Rust is not a functional language, so it is not surprising that its closures are complicated.