Let's look at the playgrounds and see if we can explain why they work, or why they don't. (I won't offer solutions/workarounds/alternatives in this post.) I'll split up the default
and return_default
functions.
Playground 2
return_default
(works)
fn return_default<T>(key: &String, model_setting: &ModelSetting<T>)
where
T: std::fmt::Debug + std::fmt::Display + std::clone::Clone,
{
if key == &model_setting.key {
println!("key: {}, default: {}", key, model_setting.default.clone());
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
}
Here you're mainly dealing with just String
s, except for the highlighted portion. There, you clone model_setting.default
, which has type T
. So the highlighted code has type T
. It's different for every T
the caller chooses, T
is the same type everywhere within the function.
default
(works)
fn default(&self, key: &String) {
for model_setting_type in &self.settings {
if let ModelSettingType::Int(model_setting) = model_setting_type {
// (A) vvvvv^^^^^^^^^^^^^
return_default(key, model_setting);
}
if let ModelSettingType::Str(model_setting) = model_setting_type {
// (B) vvvvv^^^^^^^^^^^^^
return_default(key, model_setting);
}
}
}
In this portion, model_setting
has a different type at (A) and (B), because you're matching different enum
variants. The two model_setting
s are two different variables, so it's fine that they have two different types. At (A) it's i16
and at (B) it's String
. You call return_default::<i16>
and return_default::<String>
accordingly. Every expression still has one concrete type, as required by a strongly and strictly typed language.
Playground 1
return_default
(works)
fn return_default<T>(key: &String, model_setting: &ModelSetting<T>) -> Option<T>
where
T: std::fmt::Debug + std::fmt::Display + std::clone::Clone,
{
let mut result = None;
if key == &model_setting.key {
result = Some(model_setting.default.clone());
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
result
}
As before, the highlighted portion is T
. That means result
is an Option<T>
. That matches the return type, and you don't try to assign it some other type, so there is no error.
This is a good example of how generics work: in the function body, T
is a single type that meets the bounds; the function body has to work for every such T
, and this body does so. It makes use of the Clone
bound and that's it. It doesn't try to force T
to be one specific type, or more than one type.
default
(errors)
fn default<T>(&mut self, key: &String) -> Option<T>
where
T: std::fmt::Debug + std::fmt::Display + std::clone::Clone,
{
let mut result = None;
for model_setting_type in &self.settings {
if let ModelSettingType::Int(model_setting) = model_setting_type {
// (A) ^^^^vvvvvvvvvvvvv
result = return_default(key, model_setting);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Option<i16>
}
if let ModelSettingType::Str(model_setting) = model_setting_type {
// (B) ^^^^vvvvvvvvvvvvv
result = return_default(key, model_setting);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Option<String>
}
}
result
}
Here, the model_setting
s have different types just as before, and you're calling return_default::<i16>
at (A) and return_default::<String>
at (B). In Playground 2 those both returned ()
, but now they return Option<i16>
and Option<String>
respectively, as labeled. Those are two different types -- but you're trying to assign them both to the same variable, result
.
That's the first problem -- what's the type of result
? It can't be both Option<i16>
and Option<String>
; Rust is strictly and statically typed.
We can look at it a different way, through the lens of the error message.
error[E0308]: mismatched types
--> src/main.rs:97:46
|
97 | result = return_default(key, model_setting);
| -------------- ^^^^^^^^^^^^^ expected `&ModelSetting<i16>`, found `&ModelSetting<String>`
| |
| arguments to this function are incorrect
|
= note: expected reference `&ModelSetting<i16>`
found reference `&ModelSetting<String>`
At (A) the compile decided result
must be an Option<i16>
, so at (B) where you have another assignment, it decided you must need to call return_default::<i16>
again so that it would return an Option<i16>
so that the assignment would be valid. But you passed in a &ModelSetting<String>
. It highlighted that as the error.
I find my walkthrough more intuitive, but both are ultimately just ways of showing that there's a type conflict in the code.
If we delete the if let
that contains (B), we eliminate that problem:
fn default<T>(&mut self, key: &String) -> Option<T>
where
T: std::fmt::Debug + std::fmt::Display + std::clone::Clone,
{
let mut result = None;
for model_setting_type in &self.settings {
// (A) ^^^^vvvvvvvvvvvvv
result = return_default(key, model_setting);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Option<i16>
}
result
}
But now we're trying to return an Option<i16>
when we said we would return an Option<T>
:
error[E0308]: mismatched types
--> src/main.rs:97:9
|
77 | fn default<T>(&mut self, key: &String) -> Option<T>
| - expected this type parameter --------- expected `Option<T>` because of return type
...
94 | result = return_default(key, model_setting);
| ------ here the type of `result` is inferred to be `Option<i16>`
...
97 | result
| ^^^^^^ expected `Option<T>`, found `Option<i16>`
|
= note: expected enum `Option<T>`
found enum `Option<i16>`
Compare and contrast this with return_default
, which works, keeping these things in mind:
- The caller of the function decides what
T
is (within the stated bounds)
- That's why
default
can call return_default::<i16>
or return_default::<String>
- The function body has to work for every valid choice of
T
T
has to be the same concrete type everywhere in the function body
- The
T
on fn default
and the T
on fn return_default
are independent (you just happened to give them the same name). Using U
instead of T
with return_default
doesn't change anything. This is similar to how two functions can have arguments named foo
that aren't forced to be the same type, value, etc.