Cannot infer closure type with higher-ranked lifetimes inside Box::new

add6d42
Opened by Mikhail Zabaluev at 2024-12-21 05:10:52

In the code below, type inference works in the first line of main(), but fails in the second one. Note also that the "kind" of the closure has to be given with explicit syntax when inside Box::new().

fn do_async<F>(_cb: Box<F>) where F: FnOnce(&i32) {
}

fn do_async_unboxed<F>(cb: F) where F: FnOnce(&i32) {
    do_async(Box::new(cb))
}

fn main() {
    do_async_unboxed(|x| { println!("{}", *x); });
    do_async(Box::new(|: x| { println!("{}", *x); }));
}
  1. Triage: updated code

    fn do_async<F>(_cb: Box<F>) where F: FnOnce(&i32) {
    }
    
    fn do_async_unboxed<F>(cb: F) where F: FnOnce(&i32) {
        do_async(Box::new(cb))
    }
    
    fn main() {
        do_async_unboxed(|x| { println!("{}", *x); });   // Ok
        do_async(Box::new(|x| { println!("{}", *x); }));  // ERROR the type of this value must be known in this context
    }
    

    Steve Klabnik at 2015-11-04 16:39:16

  2. From my investigation, the problem is that we don't relate the return type of Wrapper/Box::new to the argument type of api, because we are afraid there might be a coercion involved. This means the bound information is not passed to the closure. We could try to be better here - cc @nikomatsakis @eddyb.

    Ariel Ben-Yehuda at 2015-11-22 12:26:29

  3. Nominating as this is makes some API patterns horribly non-ergonomic.

    Ariel Ben-Yehuda at 2015-11-22 12:27:23

  4. triage: P-medium

    Niko Matsakis at 2015-12-03 21:37:14

  5. I agree we could probably do better here. I'm not sure what precisely is going on or the current state of this coercion code though.

    Niko Matsakis at 2015-12-03 21:37:31

  6. triage: P-medium

    Niko Matsakis at 2015-12-17 21:34:37

  7. // Works:
    fn foo<F: Fn(f32) -> f32>(_: F) {}
    foo(|x| x.sin())
    
    // Why it works: the inference variables look like this:
    foo::<$F>((|x: $A| x.sin()): $F)
    // Because $F: Fn(f32) -> f32, we can deduce $A: f32.
    
    // Doesn't work:
    fn bar<F: Fn(f32) -> f32>(_: Option<F>) {}
    bar(Some(|x| x.sin()))
    
    // Why it doesn't work: the inference variables look like this:
    bar::<$F>(Some::<$T>((|x: $A| x.sin()): $T): Option<$F>)
    // Because Some: T -> Option<T>, we know that $T = $F.
    // But they are both inference variables, so we don't replace
    // one with the other, and $T: Fn(f32) -> f32 doesn't exist.
    

    If my analysis is correct, it should be possible to fix this bug by computing inference variable equivalence via union-find instead of using simple equality.

    Eduard-Mihai Burtescu at 2016-01-08 14:58:11

  8. @eddyb

    Except that the variables are only related via a coercion.

    Ariel Ben-Yehuda at 2016-01-10 13:42:18

  9. That looks like a case of things being so loosely coupled they fall apart.

    Ariel Ben-Yehuda at 2016-01-10 13:42:54

  10. @arielb1 There's no coercion magic going on, this is the "expected" type being propagated down. Coercions are applied after the type is known. ~~Try replacing the strict equality with equivalence and it should just start working.~~

    Eduard-Mihai Burtescu at 2016-01-10 13:44:23

  11. I mean, we don't equate $T and $F, but rather we will coerce them after the call is type-checked.

    Ariel Ben-Yehuda at 2016-01-10 13:49:13

  12. @arielb1 We do to obtain the expected type for the arguments of a call.

    Eduard-Mihai Burtescu at 2016-01-10 13:49:39

  13. @eddyb

    That would be a subtype relation, which is destroyed by the commit_regions_if_ok call.

    Ariel Ben-Yehuda at 2016-01-10 13:53:32

  14. @arielb1 Let's try to be clearer using pseudosyntax:

    // Initial expression.
    bar(Some((|x| x.sin())))
    // Add type variables for bar.
    bar::<$F>(Some((|x| x.sin())))
    // Propagate expected type from signature of bar.
    bar::<$F>(Some((|x| x.sin())) expects Option<$F>)
    // Add type variables for Some.
    bar::<$F>(Some::<$T>((|x| x.sin())))
    // Propagate expected type from signature of Some.
    bar::<$F>(Some::<$T>((|x| x.sin()) expects $T) expects Option<$F>)
    

    Well, that's embarrassing, turns out I was being unreasonable. A relationship between $T and $F cannot exist before assigning the actual types.

    Presumably it might be possible to end up with:

    bar::<$F>(Some::<$T>((|x| x.sin()) expects $F) expects Option<$F>)
    

    What confused me was that the following snippet ~~works~~ seemed to work when I tried it initially:

    fn foo<F: Fn(f32) -> f32>(_: F) {}
    fn id<T>(x: T) -> T {x}
    foo(id(|x| x.sin()))
    

    Given that it doesn't actually work, it's a better testcase. If we end up with:

    foo::<$F>(id::<$T>((|x| x.sin()) expects $F) expects $F)
    

    Then that would work, but consider:

    fn foo<F>(_: F) {}
    fn id<T: Fn(f32) -> f32>(x: T) -> T {x}
    foo(id(|x| x.sin()))
    

    Where $T has the Fn bound but $F doesn't.

    @arielb1 @nikomatsakis Would it make sense to have a set of inference variable "assignments" used for the expected type? So that $T = $F is known but doesn't affect code which doesn't explicitly deal with expected types, as we only need to know that X: Fn(f32) -> f32 applies to the closure whether X is $F or $T.

    Eduard-Mihai Burtescu at 2016-01-10 14:18:04

  15. @eddyb

    That might fix this, but it may cause confusing issues if a coercion were actually to occur. Maybe have hints solely for closure inference? That looks like a hack.

    Ariel Ben-Yehuda at 2016-01-11 11:07:47

  16. @arielb1 Nothing other than closure inference looks at bounds which refer to the expected type, AFAICT.

    Eduard-Mihai Burtescu at 2016-01-11 16:32:38

  17. Triage: not aware of any movement on this issue

    Steve Klabnik at 2017-09-30 16:13:42

  18. Triage: still reproduces on 2021 edition, tested using rustc 1.59.0 (9d1b2106e 2022-02-23)

    Maayan Hanin at 2022-03-21 12:04:05