Why can't rust infer this function type? (`if` and `else` have incompatible types)

I'm still fairly new to rust and not very experienced with function types in it. I have some code that is using the minimax algorithm to evaluate game positions for a hobby project.

This simplified version of the code compiles:

pub fn minimax(maximize: bool) -> i32 {
    let (func, mut val): (fn(_, _) -> _, _) = if maximize {
        (std::cmp::max, i32::MIN)
    } else {
        (std::cmp::min, i32::MAX)
    };

    for _ in 0..2 {
        val = func(val, minimax(!maximize));
    }
    val
}

Playground

However, if I remove the type annotation from the let line:

    let (func, mut val) = if maximize {

the compiler outputs the following error:

error[E0308]: `if` and `else` have incompatible types
 --> src/lib.rs:6:9
  |
3 |       let (func, mut val) = if maximize {
  |  ___________________________-
4 | |         (std::cmp::max, i32::MIN)
  | |         ------------------------- expected because of this
5 | |     } else {
6 | |         (std::cmp::min, i32::MAX)
  | |         ^^^^^^^^^^^^^^^^^^^^^^^^^ expected fn item, found a different fn item
7 | |     };
  | |_____- `if` and `else` have incompatible types
  |
  = note: expected type `(fn(_, _) -> _ {std::cmp::max::<_>}, _)`
            found tuple `(fn(_, _) -> _ {std::cmp::min::<_>}, _)`

It works as expected without explicit types if I split that block into two separate let statements:

    let func = if maximize { std::cmp::max } else { std::cmp::min };
    let mut val = if maximize { i32::MIN } else { i32::MAX };

I have the following questions:

  1. Is this behavior expected, or should the compiler to be able to infer the type of func? Is something else going on here? If this is a known issue, any links/references would be appreciated.
  2. Is there an alternate way I can accomplish this (conditionally initializing these two variables) without repeating the if block and without explicit types? You can see a slightly less minimal/more realistic example here if that helps. I suppose I could be using fold on an iterator instead of a for loop, but I still have two separate things I want to conditionally determine -- the initial value and the reducing function.

Thanks,
Miles

Every function has its own unique type called its function item, and when you just write the name you get the function item. However, each function item type is separate, so it needs to convert it to an ordinary function pointer to be able to store multiple different functions in the variable.

This happens automatically in a few case, but apparently not in this case.

5 Likes

Coercions will not happen on inferred types. So in each case:

  1. (one if, with annotations) You specified the type of the if statement, so no inference was run. This means coercions do apply
  2. (one if, no annotations) Inference decided the type of the if statement by inferring each branch of the if statement, so coercions don't apply
  3. (two ifs) Each branch of each if statement is a literal, so no inference required. The type of the if statement can be decided by coercions.

The best way to achieve this is the first way, where you annotate the if statement. In the future you can even annotate values directly like if { ... } else { ... } : (fn(_) -> _, _), with generalized type ascription

1 Like

Thank you both for your explanations. I found Alice's reference helpful:

However, there is a coercion from function items to function pointers with the same signature, which is triggered ... when different function item types with the same signature meet in different arms of the same if or match

If I understand correctly, this seems to be a special case that isn't applied to more complex structural types that contain function items.

One other approach I hadn't previously considered would be to assign directly to variables from the conditional blocks; I'm used to this from Java but I hadn't really realized yet that Rust allows the same late initialization of immutable variables:

    let func: fn(_, _) -> _; // type annotation is still necessary for this variable
    let mut val;
    
    if maximize {
        func = std::cmp::max;
        val = i32::MIN;
    } else {
        func = std::cmp::min;
        val = i32::MAX;
    }

Yes, and since it's the immutable variables it can be initialized only once and cannot be accessed before initialization. Both are checked at compile time and produces compile error on mistake.