Generic function wants exactly same arguments in if and else branch

I have a function that reads some JSON and builds something out of it (what is not important). The only argument is an impl std::io::Read:

fn create_store_from_read<T>(read: T) -> Store
where
    T: std::io::Read,
{
    // Not important.
}

Now, I want to call the function from both branches of an if/else with different arguments:

if let Ok(file) = File::open(path) {
    create_store_from_read(file)
}
else {
    create_store_from_read("[]".as_bytes())
}

Both File and &[u8] implement Read but the compiler yells at me that the argument in the else branch is expected to be a File. I understand that both branches should "return" the same type but I cannot understand why both branches should use the same type in the called function.

Can someone enlighten me?

It's a good idea to include the full (not IDE abbreviated) Cargo build error. Is this what you're seeing?

If so, the problem isn't the arguments, it's that the if-else block must either

  • evaluate to the () type, or
  • be terminated with a semicolon

See the note on this page.

Your code evaluates to Store but has no semicolon.

I oversimplified the example code, sorry, my fault. You can find a fully working example at Rust Playground and even if I already know what the error was (I mistakenly typed impl for dyn) I don't know why the compiler presents that error. The expected File, found &[u8] really confuses me.

1 Like

The problem is AFAIK that the RPIT captures T.

Can you explain what RPIT means, please?

RPIT = Return-position impl Trait

I suggest reading this:
https://doc.rust-lang.org/nightly/edition-guide/rust-2024/rpit-lifetime-capture.html

Should you have any further questions feel free to ask.

Mm.. you mean that even if the create_store_from_read function always return the same concrete type the fact that it is generic makes the result of the call appears to the compiler as two different "hidden types" in the if/else branches even if T is not used in the return type of the called function?

If I understand this well... the code I wrote (by mistake) would work in Rust 2024 by specifying an empty use, right?

I think this deserves a diagnostics bug report

1 Like

Hmm... on second thought I also think this might actually be a bug. For my understanding captures are about lifetimes and traits but in your case it seems as if the hidden type gets somehow overriden by it.

Correct. This means the function can change to actually using something parameterized by T without breaking downstream.

You can only opt out of capturing lifetimes so far. You can use precise capturing on stable today by the way.

They're about how the opaque type is parameterized, including generic type parameters. It's not a bug (though the inability to opt out of type captures is a significant limitation).

That said, I agree with this.

2 Likes

I was curious about RPIT capturing generics so I tried it out by implementing Store for T: Read and changing the function body to use the captured T:

use std::fs::File;
use std::io::Read;

trait Store {}


struct AStore {}

impl Store for AStore {}
impl<T> Store for T where T: Read {}

fn create_store_from_read<T>(read: T) -> Box<impl Store>
where
    T: Read,
{
    Box::new(read)
}

There is no error. This means you can make the return value dependent on the generic so it has to capture it as @quinedot explained.