[Pre-RFC] Disallowing use of default type parameters

Motivation

Consider this sample code:

pub struct Tar<T = fs::File>
where
    T: Read + Write + Seek,
{
    handle: T,
}

impl Tar {
    // some impls
}

Oops, I forgot to write impl<T: Read + Write + Seek> in the impl block. I am in the middle of generalizing Tar to Tar<T>, and I am going to release a version where I forgot to generalize it for some methods!

Proposed change

Have some kind of syntax that indicates the invisibility scope of the default parameter. So for a certain scope, you cannot use the default type. I don't know a good syntax for this though. struct Tar<T = pub(crate) fs::File>? That looks very confusing, and pretty much means the opposite. Or struct Tar<T = priv(crate) fs::File> (since the priv keyword is already reserved)? That looks even more weird.

Alternative methods

@malaire on Discord suggested making this a clippy lint instead. But this lint shall be marked on the type parameter instead of for the whole crate. For example, it's very reasonable to want to declare this:

pub type Result<T, E = MyError> = std::result::Result<T, E>;

It would be bad to have warns for using Result<T> only. However, there doesn't seem to be a way for marking types for clippy's use only. I am not sure where it is feasible to implement this on clippy.

Yes, this should be a Clippy lint rather than a core language feature.

There is.

4 Likes

Hmm, I understand the sentiment, but requiring a fully fledged language feature to add a safety guard for the dev / internal workflow seems a bit overkill: since we are talking about dev workflow (e.g., CI checks being performed before releasing something), then you can set up some special env var flag that will disable the defaultness of the type parameter.

A basic version of what I mean would be doing the following:

#[cfg_attr(<your env-var triggered cfg>,
    deprecated(note = "Use `Tar_` for a version with no default type param"),
)]
pub struct Tar<T = fs::File>
where
    T : Read + Write + Seek,
{
    handle: T,
}
pub(crate) type Tar_<T> = Tar<T>;

Then, code such as

impl Tar {

will trigger the deprecation warning, which you can silence by using Tar_ instead, which does not support not being fed that type parameter.

impl<T> Tar_<T>
where
    T : Read + Write + Seek,
{

Another approach (but still dev-facing), would be to have some macro or proc-macro that would remove the default type annotations (it is quite easy to write, if anyone is interested I could publish such a helper), and then write:

#[cfg_attr(<your env-var triggered cfg>,
    remove_default_types,
)]
pub struct Tar<T = fs::File>
where
    T : Read + Write + Seek,
{
    handle: T,
}

Then, when running within the special environment context that would trigger / enable such a macro pass, the type default would disappear, and any non-generic usages such as impl Tar { will cause compilation errors.

  • Playground (Note: since this is a Playground, I had no env vars nor cargo features to play with, so I ended up using debug_assertions as a poor man's your env-var triggered cfg; obviously the presence of debug_assertions should never lead to API changes :grinning_face_with_smiling_eyes:)

  • Playground for the first deprecated-based approach


And there is the super basic and yet effective technique of just commenting out the default type param while developing, and only uncommenting it right before releasing it :wink:

2 Likes

Your example doesn't describe what I'm thinking about. I'm referring to a particular declaration/type parameter on which a default type is specified, and that the marker should be applied on it instead of at the place that triggers lint.

As I said in the example, you don't want to get your lints getting all over the place because you relied on a Result mask, but you want it to enable for specific types that you don't want to specialize.