Allow wildcard while destructuring structs

I think destructuring is a very powerful feature and it helps to write more readable code, but I can't seem to notice that other languages (such as javascript), really take advantage of this feature while in Rust it is rarely used. I blame the mandatory struct name when destructuring. Compare:

// rust yew example
#[function_component(App)]
fn foo(Properties { foo, bar, baz }: Properties) -> Html {
   let StructName { foo, bar, baz } = value(); 
   /* ... */
}
// javascript react example
function Foo({ foo, bar, baz }) {
   let { foo, bar, baz } = value(); 
   /* ... */
}

I don't think I would write code like that very often other than just to imitate the javascript example. In js this feature is used all over the place. While in Rust it is used sparingly. The struct name just gets in the way, it's redundant, and for that reason it is almost never used. This is my only explanation for the disparity anyway.

I would propose allowing either one of these:

#[function_component(App)]
fn foo({ foo, bar, baz }: Properties) -> Html {
   let { foo, bar, baz } = value(); 
   /* ... */
}
#[function_component(App)]
fn foo(_ { foo, bar, baz }: Properties) -> Html {
   let _ { foo, bar, baz } = value(); 
   /* ... */
}

The first one would be ideal, but it may be harder to parse. I believe the second one should not face any parsing limitations. I know if this were allowed I would use this feature a lot more.

This comes up fairly regularly, actually. See this post, for example, which has the same "_ { .. } syntax in a pattern" that you bring up here.

For the specific case of function arguments, this is also part of "generalized type ascription," which would allow writing

#[function_component(App)]
fn foo(Properties { foo, bar, baz }) -> Html {
   /* ... */
}

The TL;DR version of that feature is that : $ty type ascription becomes formally part of pattern syntax, and can be omitted if the type is already fully known. The fully general feature also allows writing e.g. Some(x: String) for an Option<String> pattern.

7 Likes

Which is going to give rustdoc an even more complicated version of

to resolve :sob:

I have an RFC planned for _ being used in patterns to elide a path. I created a post on here a while back with the idea.

1 Like

Somewhat non-obvious side effect here is that, from an IDE POV,

let StructName { foo, bar, baz } = value(); 

is a usage of StructName, while

let { foo, bar, baz } = value(); 

isn't.

This has direct implications for "find usages" feature, and indirect implications for some of the refactors.

For example, "convert record struct to tuple struct" refactor can process the first syntax very fast, as you don't need to run type inference to realize that StructName is a usage of the target of the refactor. With the second syntax, you now need to run type-inference to figure that out, there's no syntactic shortcut.

6 Likes

You can already do wild things like

trait Foo {
    type Ty;
}

impl Foo for [(); 42] {
    type Ty = Bar;
}

struct Bar {
    x: u32,
}

fn main() {
    let x = 1;
    type T = <[(); 40+2] as Foo>::Ty;
    T { x };
}

anyways, so the situation for such reactors is already nontrivial; e. g. in the above example you need const evaluation in order to figure out what struct is used.

6 Likes

Oh dear, we shouldn't have allowed that ideally :frowning:

EDIT: or maybe not, see End game for find usages · Issue #7427 · rust-lang/rust-analyzer · GitHub. The example isn't as bad, as the thing happens on the item level, not on the expression level.

This was actually discussed as part of the proposed "Generalized Type Ascription" back in 2018. I was originally a fan of it (RFC: Generalized Type Ascription by Centril · Pull Request #2522 · rust-lang/rfcs · GitHub), but the discussion ended up convincing me that for signatures like this having the type listed directly is important.

(That's now a bit firmer, by implication, than it was back then too, as we have pat_param in macros separate from pat.)

I once wrote this snippet of code:

// on top of file:
use input::event::PointerEvent as __;
...

// then in the main function:
for event in &mut libinput {
    if let input::Event::Pointer(
        motion_event @ (__::Motion(_) | __::MotionAbsolute(_))
    ) = event {
        ...
    }
}

This was of course dirty hack but acceptable in that situation. Through, what matters is that it seems I like how the __:: pattern looks. I've also tried to substitute paths of other enums/structs with __:: in various other places and it certainly was better than _:: e.g. the following snippet gives much worse impression:

for event in &mut libinput {
    if let input::Event::Pointer(
        motion_event @ (_::Motion(_) | _::MotionAbsolute(_))
    ) = event {
        ...
    }
}

And the same with example from the topic:

#[function_component(App)]
fn foo(__ { foo, bar, baz }: Properties) -> Html {
   let __ { foo, bar, baz } = value(); 
   /* ... */
}

vs

#[function_component(App)]
fn foo(_ { foo, bar, baz }: Properties) -> Html {
   let _ { foo, bar, baz } = value(); 
   /* ... */
}

Perhaps the __ looks better for me because it doesn't resonate with _ in patterns that suggests a plenty of options were hidden; instead it looks like a single thing (path) that was hidden because it's irrelevant. Moreover, the geometry of __ also looks interesting because it seems to attract as much attention as explicitly written name/path, or at least that's the impression I've got.

So, could we consider __:: as alternative to _:: and implicit struct/enum path inference?

I'm not sure it's better enough to overcome the friction of __ already being a valid identifier. Hopefully there aren't too many people using things like mod __;, but they could be. And it feels a bit more like Stroustrup's Rule than necessarily a substantial difference.

If we're jumping to something other than leveraging the existing "_ means 'infer this type'" we have from things like Vec<_>, then I'd rather pick a different syntax entirely. For example, I've mentioned the Swift-inspired idea of let .{ x, y, .. } = foo(); or if let .V6(ip) = get_ip() before.

2 Likes

I'm not convinced this is a substantial impact, because field references have similar issues. CAD97 had a nice example in [Pre-RFC] Inferred Enum Type - #52 by CAD97 pointing out that because the struct pattern part is infallible (otherwise it wouldn't have been inferrable) it can often be done with field access expressions instead, which are also totally fine without the type name without causing confusion, and already need to be dealt with in things like refactorings.

So I think it's only an impact if let Foo { x, .. } = y; is a "use" of Foo but let x = y.x; isn't, and I'm not convinced that that's a meaningful distinction. Or even if it's meaningful, perhaps let .{ x, .. } = y; not being a use but let Foo { x, .. } = y; being a use is no worse that let x: _ = y; not being a use but let x: Foo = y; being a use, since in both cases they're doing the same thing but differing in explicit-vs-inferred.

5 Likes

Note that in cases like this you can also use PointerEvent::* in the local scope and refer to the enum variants by unqualified names.

You could also do

use lib::MyEnum as E;

and then refer to the variants as E::Motion(..), E::MotionAbsolute(..). It's just as short, doesn't look like some magic syntax, prettier, and leaves a very obvious trace of where the variants are coming from.

3 Likes

When importing traits, I will always use use foo::Trait as _;, to bring the trait into scope without binding it to a name. Your usage of use ... as __; makes me wonder if there is potential for confusion when use ... as _; and _ { destructuring } and _::Foo like implicit enum proposals. That is what is there to indicate that this isn't Trait::Foo or Trait{ destructuring }. At the very least I don't think it would be friendly to learners of the language if used in combination.

It's personal project with ~200 LOC (half of which is boilerplate) and nobody else sees the code. The usage of __ was merely an experiment to see how it looks and I will certainly revert it if I'll decide to open-source the application.

In most of cases mod __ will be inferred with __::Item syntax. Things like foo::__::bar and even __foo::bar should also continue to work as previously. And there's no sense in something like __::Trait::AssociatedItem, so I don't think it might cause a significant friction.

Even better suggestion: the syntax for enums could be this:

// Code randomly picked from rustfmt:

fn rewrite(&self, context: &RewriteContext, shape: Shape) -> Option<String> {
    match *self {
        MacroArg::Expr(ref expr) => expr.rewrite(context, shape),
        MacroArg::Ty(ref ty) => ty.rewrite(context, shape),
        MacroArg::Pat(ref pat) => pat.rewrite(context, shape),
        MacroArg::Item(ref item) => item.rewrite(context, shape),
    }
}

// =>

fn rewrite(&self, context: &RewriteContext, shape: Shape) -> Option<String> {
    match *self {
        __ Expr(ref expr) => expr.rewrite(context, shape),
        __ Ty(ref ty) => ty.rewrite(context, shape),
        __ Pat(ref pat) => pat.rewrite(context, shape),
        __ Item(ref item) => item.rewrite(context, shape),
    }
}
let last_arg = self.result.last().unwrap();
if let MacroArgKind::MetaVariable(..) = last_arg.kind {
    if ident_like(&self.start_tok) {
        return true;
    }
    if self.start_tok == Token::Colon {
        return true;
    }
}

// =>

let last_arg = self.result.last().unwrap();
if let __ MetaVariable(..) = last_arg.kind {
    if ident_like(&self.start_tok) {
        return true;
    }
    if self.start_tok == Token::Colon {
        return true;
    }
}

Here __::Foo will unambiguously refer to module __ and I still like how the inference looks.

As a Swift person, the justification for Swift’s syntax is that the full form is SomeEnum.firstCase or SomeType.staticMember, and so shortening to .firstCase and .staticMember is a clear substring. For Rust, I think a single leading colon would be unambiguous in patterns (from the grammar’s perspective), with SomeEnum::FirstCase getting shortened to :FirstCase. But that’s (a little) harder to teach than _::FirstCase, which is a lot like Vec<_> to me.

(And ideally I want this syntax available for expressions as well as patterns, just like Vec::<_>::new(). But one thing at a time.)

1 Like

To finish the parallel, the fully swift-equivalent syntax would be ::Variant, but that already has meaning in Rust (explicitly ask for the crate namespace).

4 Likes

Note that I explicitly don't want std::_::Add to work. One of the reasons I like the swift-style syntax is because it doesn't suggest that that might work.

2 Likes

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