Skip to content

[patterns] Default to final for field destructuring #2836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
TimWhiting opened this issue Feb 9, 2023 · 3 comments
Closed

[patterns] Default to final for field destructuring #2836

TimWhiting opened this issue Feb 9, 2023 · 3 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@TimWhiting
Copy link

TimWhiting commented Feb 9, 2023

As I understand it the current way to destructure an object looks like:

Value eval(Expression expr) {
  return switch (expr) {
    Number(:final value) => IValue(value),
    Add(:final left, :final right) =>
      switch (eval(left)) {
        IValue(value: final l) => 
          switch (eval(right)) {
            IValue(value: final r) => IValue(l + r)
          }
      },
  };
}

I like the opportunity to create new names for the destructured property (as I use for the second two switches), however, it seems like writing final becomes rather cumbersome for destructuring a large amount of properties, and just adds to noise that isn't very relevant to understanding the meat of the algorithm. (Kind of like public static void main in Java).

The proposal would be to default to final variables.

Add(:left, :right) => ....

Those who want var can specify it manually. (Var is much shorter, and this style of programming typically is used in a more final way). Additionally it seems that using var as default could be more confusing to some.

Related
#136 (I think this particular issue is going to get more attention after users start using the patterns feature).

This is what the snippet would look like with the proposed change:

Value eval(Expression expr) {
  return switch (expr) {
    Number(:value) => IValue(value),
    Add(:left, :right) =>
      switch (eval(left)) {
        IValue(value: l) => 
          switch (eval(right)) {
            IValue(value: r) => IValue(l + r)
          }
      },
  };
}

I think I saw discussion in the evolution of the proposal that in the last two cases it almost looks like you are constructing an instance of IValue() until you realize the variables l and r are not in scope. I think a reasonable request would be to only default to final for when the name of the field is inferred from the pattern.

Additionally the current situation creates a weird corner of the language where the following is allowed, because of the outer variable pattern, whereas the switch case is not. (The switch case is where I would argue it is more important for readability and reduction of noise).

final (:value) = val;

At first reading through the spec, I thought the above was a mistake due to no inner variable pattern.

If the team does not want to default to final, I think a compromise of allowing something like a 'final switch' or 'final case object patterns' would be good.

// final switch with required variable pattern for renamed fields
Value eval(Expression expr) {
  return final switch (expr) {
    Number(:value) => IValue(value),
    Add(:left, :right) =>
      switch (eval(left)) {
        IValue(value: final l) => 
          switch (eval(right)) {
            IValue(value: final r) => IValue(l + r)
          }
      },
  };
}
// Final switch case object
Value eval(Expression expr) {
  return switch (expr) {
    final Number(:value) => IValue(value),
    final Add(:left, :right) =>
      switch (eval(left)) {
        IValue(value: final l) => 
          switch (eval(right)) {
            IValue(value: final r) => IValue(l + r)
          }
      },
  };
}

The final case object pattern mimics the expression variable destructuring more, whereas the final switch I think is more clean.

@TimWhiting TimWhiting added the feature Proposed language feature that solves one or more problems label Feb 9, 2023
@TimWhiting TimWhiting changed the title [patterns] Default to final for property destructuring [patterns] Default to final for field destructuring Feb 9, 2023
@munificent
Copy link
Member

I think I saw discussion in the evolution of the proposal that in the last two cases it almost looks like you are constructing an instance of IValue() until you realize the variables l and r are not in scope.

The real problem is that if you just us an identifier there, it already means something in a pattern. It's a constant test:

Value eval(Expression expr) {
  const value = 3; // <---
  return final switch (expr) {
    Number(:value) => IValue(value),
    ...
  };
}

Here, the case will match if expr is a Number whose value getter returns 3.

Unfortunately, this syntax is already taken and already means something.

I agree that it is a little annoyingly verbose to have to write var or final to destructure to variables in a pattern. But we spent months trying every possible design around this issue and it was the least bad of all of the options.

Retrofitting patterns into a language that wasn't designed for them is hard. :(

I'm going to close this because I don't think it's an approach we can take.

@munificent munificent closed this as not planned Won't fix, can't repro, duplicate, stale Apr 5, 2023
@TimWhiting
Copy link
Author

TimWhiting commented Apr 5, 2023

Retrofitting patterns into a language that wasn't designed for them is hard. :(

I'm going to close this because I don't think it's an approach we can take.

Good points, and I acknowledge all of your hard work trying to make Dart as nice as possible given the legacy / path that Dart has taken to get where it is. Thank you so much for patterns, records and everything.

However, I still think there might be a less bad option out there.

What about something like:

Value eval(Expression expr) {
  const value = 3; // <--
  return switch (expr) {
    Number(:value) => IValue(value), // <-- uses const
    Number(::value) => IValue(value), 
    Add(::left, ::right) =>
      switch (eval(left)) {
        IValue(value:: l) => 
          switch (eval(right)) {
            IValue(value:: r) => IValue(l + r)
          }
      },
  };
}

Where the double colon means create a new binding shadowing any consts that could be in scope. Or vice-versa require double colon for constants (or use the const keyword to use a const by that name)?

The double colon also has the advantage of making this binding clearly distinct from constructing an object with named parameters. Alternatively you could use other combinations of characters that don't start identifiers. Such as :> or :- etc. I kind of like :> due to the 'extract & bind' nature of this feature.

@munificent
Copy link
Member

There's always the option of introducing new punctuation that means new stuff, yes. But my experience is that you have to be really careful with it. The problem with punctuation is that it can't be "read" so it doesn't convey anything at all to a reader who's unfamiliar with it. You're basically stuck until you can figure out what it means.

That can be worth it if the punctuation happens to already be familiar from other contexts (like using + for addition) or when the operation is so common that the brevity is worth the learning tax (like maybe using .. for cascades). But often it's just confusing and not worth the savings if they're relatively marginal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

2 participants