The Fall-Through Proposal

If you're trying to optimize an enum's size it can be nice to use different variants instead of booleans. But it comes with a tradeoff: code reuse.

So you have this large enum with things like StringKey and NonValidatingStringKey and you have to copy-paste the code for StringKey into the NonValidatingStringKey match case because they do slightly different things, e.g.

match foo {
  StringKey(x) => {
    //code here
    return option.map(Some).ok_or_else(|| ValidationError(etc, etc))
  },
  NonValidatingStringKey(x) => {
    //pretty much exact same code here
    return Ok(option)
  }
}

It'd be nice if you could do something like this:

match foo {
  StringKey(x) as let skip=false | NonValidatingStringKey(x) as let skip=true => {
    //code here
    if skip {
      return Ok(option)
    } else {
      return option.map(Some).ok_or_else(|| ValidationError(etc, etc))
    }
  }
}

So you get all of the benefits of fallthrough (aka code reuse) with none of the drawbacks (because it's not fallthrough).

You can do

let (skip, x) = match foo {
    StringKey(x) => (false, x),
    NonValidatingStringKey(x) => (true, x),
}
// code here
3 Likes

I believe that if let guards are planned, but they always apply to the entire match arm. This restriction could be removed by requiring parentheses in some cases:

match foo {
    A | B if bar => {}
    // is equivalent to
    (A | B) if bar => {}

    // This could be allowed too:
    (A if bar) | (B if !bar) => {}
    (A if let x = true) | (B if let x = false) => {}
}

For these examples, yes. Now try it with this:

pub(crate) enum PatternElement<T: PatternTypes> {
    Arrow,

    Identifier(usize),

    StringKey(usize),
    RegexKey(usize),
    KeySubtree(usize),
    ValueSubtree(usize),
    Paramemter(usize),
    ApplyPredicate(usize),

    SkipStringKey(usize),
    SkipRegexKey(usize),
    SkipKeySubtree(usize),
    SkipValueSubtree(usize),
    SkipParamemter(usize),
    SkipApplyPredicate(usize),

    End
}

I don't follow. What is it that you're trying to achieve, and why is current syntax not good enough?

match pattern_element {
    case StringKey(key) | SkipStringKey(key) {
        let skip = matches!(pattern_element, SkipStringKey(_));
        // ...
    }
    // etc
}

Yeah, that's what we're currently using. We believe it has some issues that need to be acknowledged tho, like accidentally using different pattern_elements in the match and in the matches!.

You could also refactor (or have a translated type) such that you have something like:

struct Whatever {
  skip: bool,
  whatever: EnumWithoutTheAlmostDuplicateVariants,
}

You could also refactor (or have a translated enum) so that your variants look more like:

pub(crate) enum PatternElement<T: PatternTypes> {
  StringKey(usize, bool),
  // No SkipStringKey
}

This one is particular is basically @CAD97's suggestion, factored out into a translation method (From impl maybe) so that there's only one place to make those mistakes.

A key important thing to note here is that this doesn't have to be the version of the enum that you store and pass around. It can be a transient form that you just create temporarily to make it easier to work with.

1 Like

Hmm...

We still gain the memory usage savings when doing that?


Also: What do we lose from not having a replacement for fall-through? Rust has enums, as a replacement for tagged unions. Rust uh wait did we ever merge/stabilize label-break-value? as an alternative to goto. Having something that's more restrictive than fall-through, but still enables some common fall-through-related ergonomics, would be nice.

You could solve that by creating an fn is_skip(&self) -> bool method and just calling it before (or after) you match. You can always mis-write code. The normal solution to that kind of thing is to separate concerns behind different abstractions.

Separating concerns behind different abstractions is the problem tho. By keeping things together, you reduce risk of mistakes.

(Foo if let x=true) | (Bar if let x=false) would be nice.

1 Like