Skip to content
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

Parameter destructuring #3001

Open
GregoryConrad opened this issue Apr 17, 2023 · 10 comments
Open

Parameter destructuring #3001

GregoryConrad opened this issue Apr 17, 2023 · 10 comments
Labels
feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching.

Comments

@GregoryConrad
Copy link

GregoryConrad commented Apr 17, 2023

Looked for an issue on this, but did not find one. This was briefly mentioned in #2774:

This is not about patterns in parameters, which we should also have

In Rust, you can do:

struct Abc {
  i: i32,
}

fn example(Abc{i}: Abc) -> i32 { i }

fn example2((a, b): (i32, i32)) {}

This sort of syntax would be very nice to have in Dart, like:

class Abc {
  int i;
}

int example(Abc(i)) => i;

void example2((int a, int b)) {};

example(Abc(1234));
example2((1, 2));
@GregoryConrad GregoryConrad added the feature Proposed language feature that solves one or more problems label Apr 17, 2023
@lrhn
Copy link
Member

lrhn commented Apr 17, 2023

Definitely something I want.

I really want declaration patterns everywhere a value is checked/ensured to be a type.

  • Parameters: void magnitude(var (:int x, :int y) vector) => sqrt(x * x + y * y);
  • Capturing exceptions: try { ... } on FormatException(:var source, :var index) { .... }. (Won't give you the stack, but you shouldn't need a stack for an exception, only for errors.)
  • Inline classes with primary constructor syntax: inline class MyPair(var (:int x, ;int y)) { ... }, where x and y are in the lexical scope of the class.
  • Extensions: extension Foo on Pair(:int x, :int y) { .... } where x and y become lexically scoped variables.
    (But I'd probably go for a primary-constructor-like syntax instead: extension Foo(var Pair(:int x, :int y)) { ... }
    if we get that syntax for other things.)

The biggest question here is whether the leading var is needed for parameters. Is it:

int example(var Abc(:i)) => i;
// or
int example(Abc(:var i)) => i;

The former is more consistent with existing declaration patterns, so I'd say it's the more likely approach.

The alternative is to see a parameter list as one big declaration pattern to begin with, so no extra var nesting is needed. Like var ((:int x, :int y), Color c) = cp; is a declaration pattern, we could have fromCP((:int x, :int y), Color c) as parameter list, the var being implicit, like it already is in parameters, (x) => x is valid.

The other issue is that we want to give a parameter a name, even though a pattern declaration doesn't have one.
We can't do var (:int x, :int y) pair = ...;, but we may want to give a name to the parameter foo(var (:int x, :int y), otherwise we cannot document it. And if it's a named parameter, it definitely needs a name, so:

void foo({(:int x, :int y) point}) => ...

needs the name. (And then, can point refer to the entire record inside foo?)

Some design space to go through here.

@ykmnkmi
Copy link

ykmnkmi commented Apr 17, 2023

Will this be possible:

void printXY(({int x, int y, _ /* ignore rest */})) {
  print('x: $x, y: $y');
}

printXY((x: 0.0, y: 0.0));
printXY((x: 0.0, y: 0.0, z: 0.0));

@GregoryConrad
Copy link
Author

GregoryConrad commented Apr 17, 2023

@ykmnkmi That's a good question; I'm curious as to the response on that.

@lrhn about named/positional parameters: at a certain point here, I think it would almost make sense to just unify the function parameter and record syntax since they are so similar, and then this issue would probably become much easier to handle from a design perspective. (At that point, I believe you wouldn't need to worry about named parameters anymore, and can instead just focus on the names used within the destructuring?)

I remember seeing mention of the Dart/Flutter teams wanting a syntax like this:

MyWidget(
  positionalArgument,
  someNamedParam: 1234,
  if (someOtherProperty) someOtherNamedParam: Foo.bar, // conditionally passing argument

Wouldn't that also require parameters to be treated in a similar manner to records? Maybe this issue could be tackled together with that one. (Thought process: if you can conditionally pass an argument to a record, and function parameters are treated similarly to records, then it'd be much easier to conditionally pass an argument to a function?)

@GregoryConrad
Copy link
Author

Actually just had another idea too; if #2899 or #2232 were to be introduced, then conditionally passing in an argument to a record/function would become easier too! Any variables not passed in would be assigned as default or null.

@munificent
Copy link
Member

I'm with @lrhn in that I would like to support destructuring patterns in parameter lists, yes.

But Dart 3.0 won't support that. The problem is that the parameter list syntax is already very complex and, in particular, uses [ ] and { } to mean something other than list and map patterns. Not to mention the old function-typed parameter syntax which clashes with object patterns.

So figuring out how to jam some or all of the pattern syntax into that already dense (and, frankly, not that great) parameter grammar is really hard, possibly intractable. One thing we probably could do is support destructuring record fields when a parameter type is a record type annotation. This wouldn't allow arbitrary patterns in parameters, but would at least allow:

takePair((int x, int y) pair) {
  print('$x $y');
}

main() {
  var pair = (1, 2);
  takePair(pair);
}

Will this be possible:

void printXY(({int x, int y, _ /* ignore rest */})) {
  print('x: $x, y: $y');
}

printXY((x: 0.0, y: 0.0));
printXY((x: 0.0, y: 0.0, z: 0.0));

No, it won't. We don't do "width subtyping" for records, and we don't allow you to work with "part" of a record whose remaining shape is unknown. Being polymorphic over record shapes in this way can lead to a lot of complexity and potentially make code size and performance worse. To avoid that, whenever you're working with a record in Dart, the compiler always knows its entire shape and all of its fields.

@benthillerkus
Copy link

benthillerkus commented Oct 26, 2023

Hey, I wanted to add a use case: When using indexed on an Iterable:

int getMaxChildIndexForScrollOffset(double scrollOffset) {
  return myItems
    .indexed
    .lastWhere((_, final item) => item.startOffset + item.height <= scrollOffset)
    .$1;
}

Edit:

Regarding pattern matching in Rust, the Axum web framework makes heavy use of that feature, in that the User can declaratively specify what they want their handlers to handle, instead of manually having to destructure and Deserialize Requests, Query-strings or JSONs and then rejecting if they don't match.

Obviously this is all implemented by that crate and not the language feature that allows destructuring / pattern matching in that position, but I think it serves as a less academic example for why it's a useful feature.

docs.rs/axum/latest/axum/extract/index.html#common-extractors

@lukehutch
Copy link

lukehutch commented Nov 7, 2023

I tried a few variants of the following, and was surprised to find out that pattern matching doesn't work in arg lists:

Map<String Map<String, int>> mapOfMaps = ...;
mapOfMaps.entries.map(({String k: Map<String, int> v}) => ...);

(Yes, I know I can just do mapOfMaps.map((k, v) => ...))

@Lootwig
Copy link

Lootwig commented Jan 12, 2024

Hey, I wanted to add a use case: When using indexed on an Iterable:

int getMaxChildIndexForScrollOffset(double scrollOffset) {
  return myItems
    .indexed
    .lastWhere((_, final item) => item.startOffset + item.height <= scrollOffset)
    .$1;
}

@benthillerkus on an unrelated sidenote on that example, there is List.lastIndexWhere() for what you're doing there :)

@adriank
Copy link

adriank commented Jan 12, 2024

@munificent In Dart we can do this:

fn(data) {
  final (
    :int a,
    :String b,
  ) = data;
  print('$a, $b');
}

but not this:

fn((
  :int a,
  :String b,
)){
  print('$a, $b');
}

I don't see why this would interfere with [] and {}. It's just addition.

@kswoll
Copy link

kswoll commented Mar 24, 2024

I strongly agree that Dart would be greatly improved by being able to destructure records in lambda parameters. In the meantime, here's a workaround we use for at least simple record types:

/// Handles records with two values.
extension RecordDestructurer2<T1, T2> on Iterable<(T1, T2)> {
  /// Since lambdas do not allow destructuring, this is a workaround so you can
  /// avoid more verbose code or using the $x syntax.
  Iterable<TResult> destructure<TResult>(
    TResult Function(T1 value1, T2 value2) callback,
  ) sync* {
    for (final (value1, value2) in this) {
      yield callback(value1, value2);
    }
  }
}

Usage:

final list = [(1, 2), (3, 4)];
final [value1, value2] = list.destructure((x1, x2) => x1 + x2).toList();
expect(value1, 3);
expect(value2, 7);

Hope this might be useful to others. The pattern supports more boilerplate (eg. RecordDestructurer3) to support a larger number of values in the record without worrying about colliding with the destructure method name, since the type on which the extension is applied is technically distinct.

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 patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests

9 participants