Ergonomics around wrapping thread creation / Passing closures to closures

Closures are great. Very convenient for quickly defining a function to factor out some logic. Especially how they infer types.

Until I want to factor out some code when I'm starting a handful of threads. Then the twin facts that every closure is its own type, and that we apparently don't have generic closures yet rear their ugly heads.

If I want to pass closures as parameters to closures, are these my only options?

  1. Upgrade the outer closure to a function, and have to explicitly write out all the trait bounds, which are potentially long and ugly (because apparently functions don't have type inference like closures)
  2. Change the outer closure to a macro. (probably the easier of the two options, but feels like giving up)

I would say, broadly: find a factoring that has nicer types and bounds. Sometimes that’s not possible.

There's got to be a better way.

    let (tx, rx) = std::sync::mpsc::channel();
    fn spawn_named(
        tx: &std::sync::mpsc::Sender<
            std::result::Result<
                (),
                std::boxed::Box<dyn std::any::Any + std::marker::Send + 'static>,
            >,
        >,
        name: &str,
        f: impl FnOnce() -> () + std::marker::Send + std::panic::UnwindSafe + 'static,
    ) {
        let tx = tx.clone();
        std::thread::Builder::new()
            .name(name.to_string())
            .spawn(move || {
                tx.send(std::panic::catch_unwind(|| f()));
            })
            .unwrap();
    }

I would say that the function signature is reasonable, but it is tedious to read because of style issues. Here’s how I would polish it up:

use std::{fmt, panic, sync::mpsc, thread};

fn spawn_named(
    tx: &mpsc::Sender<thread::Result<()>>,
    name: impl fmt::Display,
    f: impl FnOnce() + Send + UnwindSafe + 'static,
) {
    let tx = tx.clone();
    thread::Builder::new()
        .name(name.to_string())
        .spawn(move || {
            _ = tx.send(panic::catch_unwind(f));
        })
        .unwrap();
}

Note in particular the use of std::thread::Result. And if that type alias did not already exist, you could define it yourself, for the sake of naming this Sender and its corresponding Receiver type.

2 Likes

There might be niche alternatives depending on the situation, now or in the future.

1 Like

I had to double check whether you are the person who always avoid using use statements.

Not me, but I have been drifting in that direction. It was saving me a few headaches, but there is a clippy lint for it, I suppose, so I'll change my ways.

That lint is marked as "restriction", which means that the lint does not represent an endorsement of a particular coding style. It's just a thing you can enable on a lint-by-lint basis for specific purposes.

...I'm going to have to find a narrower font...

I personally import most of the stuff :person_shrugging: