How to "name" impl FnOnce which is never instantiated?

In my experiments with generics I dug myself into a hole where I need to provide a type for a throwaway impl FnOnce which is never called.

More specifically, I need to instantiate Either::Right where Either:Left holds FnOnce(()).

use either::*;

// How to provide impl FnOnce without a closure?
fn make_right<R>(r: R) -> Either<impl FnOnce(())->R,R> {
    Right(r)
}

My current solution is cumbersome and I suspect there is a better way:

use either::*;

// Let it infer FnOnce from dummy closure, then set Right value
fn make_right<R>(r: R) -> Either<impl FnOnce(())->R,R> {
    let dummy = |()| -> R { unreachable!() };
    let mut x = Left(dummy);
    x = Right(r);
    x
}

You can use any concrete type that implements FnOnce(()) -> R. That includes closures with such a signature, but also plain function pointers. So you can make your make_right function a lot easier by using fn(()) -> R instead of inferring the type from a dummy closure:

fn make_right<R>(r: R) -> Either<impl FnOnce(()) -> R, R> {
    Right::<fn(()) -> R, R>(r)
}

Playground.

3 Likes

Maybe my favorite approach I found for such cases is to use uninhabited closure types. Slightly tedious, but some neatly optimal layout at run-time; the Either enum would become essentially (in practice; not a compiler guarantee) a transparent wrapper in this case, as you can see by how the size of the Either is still the same as the u32 itself.

use either::*;

// How to provide impl FnOnce without a closure?
fn make_right<R>(r: R) -> Either<impl FnOnce(())->R,R> {
    #[allow(unreachable_code)]
    if false {
        let i: std::convert::Infallible = unreachable!();
        return Left(move |_| {
            let _ = &i; // actually capture i in edition 2021
            match i {}
        });
    }
    Right(r)
}

fn main() {
    dbg!(std::mem::size_of_val(&1_u32));
    dbg!(std::mem::size_of_val(&make_right(1_u32)));
}
     Running `target/debug/playground`
[src/main.rs:17] std::mem::size_of_val(&1_u32) = 4
[src/main.rs:18] std::mem::size_of_val(&make_right(1_u32)) = 4

whereas your original solution would output

[src/main.rs:12] std::mem::size_of_val(&1_u32) = 4
[src/main.rs:13] std::mem::size_of_val(&make_right(1_u32)) = 8

and the solution of using fn pointer types would be even larger, giving

[src/main.rs:12] std::mem::size_of_val(&1_u32) = 4
[src/main.rs:13] std::mem::size_of_val(&make_right(1_u32)) = 16

(on a 64 bit machine)

3 Likes

Interesting, why does it work if we specify fn , but not FnOnce? Is fn a concrete type somehow?

Yes. fn is the primitive type for function pointers and FnOnce is a trait.

1 Like

Yes. Even though it (unfortunately) looks a lot like function items, the type fn(…) -> … is actually the type for function pointers, so it’s a single concrete type (and not the type of actual functions in Rust [those have individual anonymous zero-sized types], though functions [and non-capturing closures] can be implicitly converted into function pointers).

1 Like

Fascinating, is it what unstable ! for, but without any nightly features? Why does your capture trick work, but something like Right::<fn(Infallible) -> R, R>(r) doesn't?

Because fn(!) -> R is inhabited. It can't be called, but can easily be created - this code, for example, compiles successfully:

fn test<R>() {
    let _: fn(std::convert::Infallible) -> R = |x| match x {};
}
3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.