Rust: Tracking issue for `invalid_type_param_default` compatibility lint

Created on 1 Oct 2016  路  16Comments  路  Source: rust-lang/rust

This is the summary issue for the invalid_type_param_default
future-compatibility warning and other related errors. The goal of
this page is describe why this change was made and how you can fix
code that is affected by it. It also provides a place to ask questions
or register a complaint if you feel the change should not be made. For
more information on the policy around future-compatibility warnings,
see our breaking change policy guidelines.

What is the warning for?

Type parameter defaults outside of type declarations were never intended
to be permitted, but some early versions of Rust did accept them. For
example:

struct Foo<T=i32> { 
    // the default of `i32` is legal here, since this
    // is a type declaration
}

impl<T=i32> Debug for Foo<T> { .. }
//   ^^^^^ default not legal here, in an impl

fn bar<T=i32>(x: T) { }
//     ^^^^^ default not legal here, in a fn

When will this warning become a hard error?

At the beginning of each 6-week release cycle, the Rust compiler team
will review the set of outstanding future compatibility warnings and
nominate some of them for Final Comment Period. Toward the end of
the cycle, we will review any comments and make a final determination
whether to convert the warning into a hard error or remove it
entirely.

Current status

A-lint B-unstable C-future-compatibility T-lang

Most helpful comment

Ok, I'm now curious... where can I read about other possible meanings? The expectation seems pretty straightforward, just like in case with struct - that with fn do_smth<T=SomeType>() {}, do_smth() would be equivalent do do_smth::<SomeType>() but maybe I'm missing some issues?

All 16 comments

@nikomatsakis, could you write a description for this?

@petrochenkov done.

Where can I find discussion of this issue? I don't see any on RFC 213, nor its tracking issue (#27336).

@nrc perhaps I should have phrased the summary with slightly differently. I think the main thing is that defaults were being accepted in stable code but ignored, unless you opt in to a feature gate, which gives them some semantics (but semantics that I now think I disagree with).

Is there a reason this is being phased out on functions?

It's sometimes useful to have fn do_smth<A, B=A>(a: A, b: B) { ... }.

@RReverser Yes. Because, IIRC, they are basically ignored on stable Rust. On nightly Rust, meanwhile, we were considering various possible meanings, but have not yet found one that we are satisfied with.

Ok, I'm now curious... where can I read about other possible meanings? The expectation seems pretty straightforward, just like in case with struct - that with fn do_smth<T=SomeType>() {}, do_smth() would be equivalent do do_smth::<SomeType>() but maybe I'm missing some issues?

I'm also curious as to what the issue is for default type arguments in generic functions. I'm running into a case where it would greatly help ergonomics of using a function with an optional generic parameter: https://is.gd/Gy9bXW

I think this feature is very useful for API evolution e.g. when you add a generic type parameter and want to keep stability, and I think much better than inferring the type instead.

Apparently there are some issues with the implementation. Can they maybe be fixed instead of removing the feature? Would that require an RFC?

I think I see what the difficulty is. Bear with me:

The expectation seems pretty straightforward, just like in case with struct - that with fn do_smth<T=SomeType>() {}, do_smth() would be equivalent do do_smth::<SomeType>()

So, yes, this is exactly how it works for types and traits, and it is exactly what we don't want for functions. Let's look at @randomPoison's example. Here's what we would love to be able to write, correct?

fn main() {
    let _ = foo(Some("I'm a string"));
    let _ = foo(None);  // ERROR: type annotation needed
}

pub fn foo<S = String>(opt: Option<S>) -> String
    where S: Into<String>
{
    opt.map(Into::into).unwrap_or_default()
}

Okay, now, let's assume generic type parameters for functions worked just like those for types. Turns out this code still wouldn't compile. Why? Because it would be equivalent to this:

fn main() {
    // nothing was specified, so use the default
    let _ = foo::<String>(Some("I'm a string")); // ERROR: &str vs String

    // nothing was specified, so use the default
    let _ = foo::<String>(None);
}

when what we really want is this:

fn main() {
    // infer the type *if possible*...
    let _ = foo::<_>(Some("I'm a string"));

    // ...otherwise, use a default
    let _ = foo::<String>(None);
}

which appears to require a far greater application of pixie dust.

@exphp Personally I'd rather prefer the explicit default to be used in that example over the contextual inference, although I can see some people wanting the opposite. More common usecase would be where type can't be inferred at all, just like currently used for optional type params in structs like HashMap.

@ExpHP That's a good point. I guess what I really wanted was more of a default type hint, i.e. "always try to infer the correct type, and if there's not enough information to infer a type, use the specified type as the default. That's probably a different feature than the one being discussed in this issue, though.

@randomPoison

I guess what I really wanted was more of a default type hint, i.e. "always try to infer the correct type, and if there's not enough information to infer a type, use the specified type as the default".

It's not clear how exactly this should be done, and this is the single most important blocker for progress on this issue and all related features (e.g. https://github.com/rust-lang/rfcs/pull/2176).
See https://github.com/rust-lang/rust/issues/27336 (including links to other threads) for some history.

Is there a plan to make this work for generic associated types e.g. in the following code (playground link)? At the moment, this trips the invalid_type_param_default lint.

#![feature(generic_associated_types)]

use std::ops::Deref;

trait SomeTrait {
    type Assoc<T = i32>: Deref<Target = T>;
}

fn main() {
}

@ExpHP

fn main() {
    let _ = foo(Some("I'm a string"));
    let _ = foo(None);  // ERROR: type annotation needed
}

pub fn foo<S = String>(opt: Option<S>) -> String
    where S: Into<String>

I don't think this is the point of using default type parameters in functions. In your example it could easily be deduced what the type of foo(None) should be, simply the same as foo::<String>::(None), as the type parameter was omitted and the default kicks in.

I have a use-case where I want to be able to apply a default value to a trait impl:

enum FilterOp<T=()> {
    Binary(T),
    Unary
}
trait Filter<T=()> {
    fn filter(self, field: &str, op: FilterOp<T>) -> Self;
    ...
}

impl<T=()> Filter<T> for Query {
    fn filter(self, field: &str, op: FilterOp<T>) -> Self {
        ...
    }
}

And then use the function filter with FilterOp::Unary:

query.filter("field", FilterOp::Unary) // should use default -> FilterOp<()>

But for some reason filter doesn't apply the default for FilterOp<T> and it is not allowed to define a default in the trait implementation of Filter<T>.

I have opened a thread on the rust-lang user forum: here

In your example it could easily be deduced what the type of foo(None) should be, simply the same as foo::<String>::(None), as the type parameter was omitted and the default kicks in.

My point was that foo(Some("I'm a string")); would suddenly stop working for the same reason.

impl<T=()> Filter<T> for Query {
    fn filter(self, field: &str, op: FilterOp<T>) -> Self {
        ...
    }
}

Defining a default in a impl seems like a wild idea. Impls could come into play for all sorts of reasons, and before long you could have two impls trying to specify conflicting defaults for the same type parameter. (this can happen with functions too, but for impls its more subtle because they are not explicitly named in the function body!)

But for some reason filter doesn't apply the default for FilterOp<T> and it is not allowed to define a default in the trait implementation of Filter<T>.

The reason FilterOp::Unary doesn't use the type default is because defaults currently only apply to types, not expressions. FilterOp in this case is only an ident in a path expression. Surrounding it in angle brackets (for UFCS) however would force it to be parsed as a type:

written            equivalent
FilterOp::Unary    FilterOp::<_>::Unary
<FilterOp>::Unary  <FilterOp<()>>::Unary

Like it or not, I'm pretty sure it would be a breaking change to make the former behave like the latter, as there may be code out there which currently uses an enum isomorphic to FilterOp, but where the variant similar to Unary is used for T not necessarily equal to the default. (inferred by later function calls)

Was this page helpful?
0 / 5 - 0 ratings