Should Fn() -> T implement Into<T>?

Imagine you have a function like this

fn foo(a: impl Into<u32>) -> u32 {
    a.into()
}

Wouldn’t it be nice if you could also pass in a closure that takes no arguments and produces the expected type?

foo(|| 10);

Discuss.

2 Likes

You can't impl<F: Fn()->T> Into<T> for F because it could conflict with the blanket impl - unavoidably so, because a function object could hypothetically return an instance of the same type.

6 Likes

I see two problems:

  • You can already implementations like this for specific types,

    struct Foo;
    
    impl<F> From<F> for Foo
    where
        F: FnOnce() -> Foo,
    {
        fn from(f: F) -> Self {
            f()
        }
    }
    

    (playground)

    those implementations could have different behavior and would be in conflict with adding a generic implementation in the standard library

  • It’s possible (although not on stable Rust AFAICT) to have a type Foo implement FnOnce() -> Foo itself, so there’s the potential for conflict with the generic T: Into<T> / T: From<T> implementation in the standard library

    #![feature(unboxed_closures, fn_traits)]
    
    struct Foo;
    
    impl FnOnce<()> for Foo {
        type Output = Foo;
    
        extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
            Foo
        }
    }
    

    (playground)

Which blanket implementation?

To your first point, it’s not the same.

To your second point, there’s always the name resolution syntax.

The one steffahn mentioned as the second point. It's a conflicting impl, where the two impls would implement the same trait for the same concrete type, which is a hard error in rust - Universal Function Call Syntax would not be sufficient to disambiguate between them.

I think the coherence and conflict problems could be avoided if the compiler generated an impl Into for each individual closure type, rather than a generic impl for all Fn() -> T types.

3 Likes

I also think that the problems are not insurmountable. The question is if people think it’s a good idea.

Well that's an interesting thought. It seems like it would conflict with (currently legal) impls like the one steffahn posted, though.

Users can't implement traits for anonymous closure types, so there's no danger of conflicting impls. Foo in the example above is not a “closure type” (in the sense of “type of a closure literal”).

2 Likes

No. The documentation for From says the following:

Note: This trait must not fail.

Here the conversion can fail because the function can panic.

2 Likes

But Foo implements From<F>, causing any closure type that returns Foo to implement Into<Foo> due to the blanket impl of Into. Surely that would conflict with an explicit impl of Into<Foo> for such a closure?

4 Likes

I interpret fail as return an error not panic.

1 Like

Oh, you're right. I misunderstood the source of the conflict.

1 Like

Can’t you not add it to those that already have an implementation?

Leaving aside coherence issues, this impl seems potentially problematic in generic contexts. I think this would be an unexpected impl of into(), and I think it's not unreasonable to instead require the caller to invoke the closure and pass the result.

I don't see what benefit this provides, since most of the time .into() gets called near the top of the function anyway, so the same code would run in the same order.

4 Likes

It could help unify APIs like Option::unwrap_or and Option::unwrap_or_else, since T: Into<T>.

2 Likes

That could be accomplished via a trait if it were desired — it doesn't have to be From/Into

1 Like

Wouldn't any trait for that job end up having the same problem as this one? Because a custom function object can be both a T and a FnOnce()->T?

1 Like

What's wrong with the status quo? I'd actually think that the semantic distinction between just a default value vs. lazily evaluating an entire path of control flow matters. Nobody in reality writes code like .unwrap_or_else(|| 42) if they can just write .unwrap_or(42).

3 Likes