(Pre-)RFC: Implied type for patterns in argument position

Summary

Allow irrefutable named struct, tuple struct and single-variant enum (but not bare tuple) patterns in argument position to elide type specifiers when those types are not generic.

Motivation

The current syntax is redundant - using a destructuring pattern in a function signature requires specifying the type twice, once within the pattern and again in "normal" position. For cases where this is allowed in the first place, it isn't providing any new information, as there is only one possible choice of input type for a given pattern, which is even named within the pattern itself. This is particularly noticeable for newtypes, where we may have a type exist to hang traits from or to enforce that data is provided in the right "units," while the actual logic of our function immediately unwraps its nominal input because it is, e.g. a message handler unwrapping a payload.

Explanation

It is already possible to use a pattern binding to destructure the arguments being passed into a function, like this:

fn my_func(Struct{foo, bar} : Struct, TupleStruct(baz, qux): TupleStruct) {
/* Can now use foo, bar, baz, quz as locals */
}

With implied type specifiers, the following is also valid for irrefutable patterns on non-generic named structs and tuple structs, where it means the same thing as above:

fn my_func(Struct{foo, bar}, TupleStruct(baz, qux)) {
/* Ditto */
}

This is intended as purely syntax sugar. The requirements dictating when destructuring is allowed do not change at all. Similarly, the details of pattern match syntax itself, and the semantics and lifetimes of the destructured fields, remains the same.

Because the language specifically avoids features that implicitly infer in type signatures, this would specifically exclude allowing the same syntax with bare tuples.

fn my_other_func((foo,bar)) {
/* Not allowed */
}

Drawbacks

It complicates the grammar and introduces a new special case. There doesn't seem to be any other apparent problem - it doesn't appear to preclude anything or complicate the language significantly.

Rationale and alternatives

  • Don't do this

Prior art

The only language I know of that has pattern matching directly on function arguments like this is Haskell. However, while Haskell makes argument deconstruction a fundamental part of the language, the story there is very different because it's primarily applied to refutable patterns on generic arguments. The langauge also makes the top-level type signature entirely optional, so even in the analogous case, Haskell users don't have the same overhead. This RFC (and AFAIK any likely RFC in the future) is not nearly as powerful as Haskell's capabilities, so it's hard to make a meaingful comparison.

Unresolved questions

  • Normally in argument position, &Pattern { field } is useless because it can only be meaningfully applied when typed as &Pattern, which in turn results in trying to move field through a reference. This change as currently written does not change that meaning, so implied type specifiers could only be used for consuming values, not taking by reference. Is it worth special-casing &Pattern { field }, with no type specifier, to instead desugar into Pattern {field} : &Pattern? (And similarly for &mut Pattern { field }, etc)

  • Is there a natural meaning for an elided generic type? Struct<T> { thing } is currently a grammar error, so the generic types can't be explicitly specified without changing that. Having the fields be generic over the appropriate bounds (ala impl Trait) is potentially very awkward to use, while inferring their types from use in the body is essentially global inference by the backdoor.

Future possibilities

  • Allowing refutable patterns in an argument context is an obvious extension of this idea, but is left to a future RFC as it 1) is technically orthogonal to this one, since pattern matching already exists, 2) impacts much more on the language's functionality than the syntax, and 3) is much less obvious in its value and implications.
5 Likes

I think this is fine for structs, and uni-variant enums, but not tuples, because we can't infer the type of the tuple from the signature alone. I like this.

2 Likes

This is a followup to RFC#2522: Generalized Type Ascription by @Centril that used to be part of it (but was apparently deferred at some point). The example from the draft version was fn foo(Wrapping(alpha: usize)) {}.

I'm definitely personally in favor of allowing ascription to apply to only part of (or none of) function arguments so long as the type is uniquely specified by what type information is there.

3 Likes

I agree that it would be nice to avoid writing the same type twice in signatures.

I wonder if we could have more general purpose type inference when destructuring?

fn my_func({foo, bar} : Struct, (baz, qux): TupleStruct) {
/* Can now use foo, bar, baz, quz as locals */
}

This could also be used outside signatures.

3 Likes

The generic parameters can be specified,here is an example:

#[derive(Default)]
struct Foo<T> {
    x: T,
    y: T,
}

fn foo<T>(Foo::<T> { x, y }: Foo<T>)
where
    T: std::fmt::Debug,
{
    dbg!(x, y);
}

fn main() {
    foo(Foo::<u32> { x: 5, y: 8 });
}

2 Likes

@RustyYato Just to clarify, this proposal specifically excludes bare tuples, i.e nameless structs. I propose it should work for named structs with unnamed fields (struct Foo(bar, baz) style), as the struct name implies the types. Is that what you had in mind?

@alexheretic My immediate thought about that particular syntax is that it implies structural (rather than nominal) destructuring in some sense, but I'm not sure if that means anything if a concrete type is always required on the RHS.

@matt1985 thanks, I guess there isn't a problem with doing this for generics after all

1 Like

Not always. Sometimes you have to pass an argument by reference even though it implements Copy, e.g. when passing the function to Iterator::filter. Then it makes sense to immediately destructure the reference.

Here's a crazy idea:

There was a thread recently proposing to omit the enum name when pattern-matching on an enum. This could be extended to structs:

struct Foo {
    a: u8,
    b: u8,
}

enum Bar {
    C,
}

match (Foo::new(), Bar::C) {
    (_ { a, b }, _::C) => todo!(),
}

fn baz(_ { a, b }: Foo, _::C: Bar) {}
1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.