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

Inferring required named parameters without making function types a pitfall #3287

Open
munificent opened this issue Aug 17, 2023 · 8 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@munificent
Copy link
Member

munificent commented Aug 17, 2023

In a function declaration like:

foo({int x = 1, int y}) { ... }

The declaration of x is fine. It's non-nullable but optional, since it has a default value. The declaration is y is an error since it's optional (no required), has a non-nullable type, and no default value.

If we wanted to make y not an error, the obvious interpretation is to infer required. We don't do that because that would lead to an asymmetry in function types:

typedef TakeTwo = Function({int x, int y});

This typedef is fine and both x and y are optional even though they have non-nullable types and no default value.

We could say that function declarations and function types just work differently: We infer required in the former and optional in the latter. But that's a pitfall for users. If they take a function declaration and copy/paste its signature to a function type or typedef the resulting type might be different.

Function declarations are much more common than function types. So this rule punishes the common case in order to avoid problems with the rare case. I have an idea for how we could make function declarations smarter and shorter while avoiding this pitfall.

Proposal

The proposal is pretty simple:

  • Infer required for named parameters in function declarations that have potentially non-nullable types and no default value. So in this declaration:

    foo({int x = 1, int y}) { ... }

    There is no error and y is a required named parameter.

  • Require function types to be explicit for potentially non-nullable named parameters. Instead of inferring optional (current) or required (what I propose doing for function declarations), it just forces the user to choose. This is an error:

    typedef TakeTwo = Function({int x, int y});

    It doesn't know whether x and/or y should be required. To resolve this, you must explicitly mark the parameter as being required or optional. If you change it to:

    typedef TakeTwo = Function({optional int x, required int y});

    Now there is no error. The first parameter is optional and the second is required.

    Note that this only applies to potentially non-nullable parameter types. Nullable parameter types are inferred optional if there is no required as they are today and as they are in function declarations.

This will make function types that have optional named parameters of potentially non-nullable types more verbose. In return, it will make function declarations that have required named parameters of non-nullable types more succinct.

For example, this class in Flutter gallery:

class _ColorPickerSwatch extends StatelessWidget {
  const _ColorPickerSwatch({
    required this.color,
    required this.selected,
    this.onTap,
  });

  final Color color;
  final bool selected;
  final Function? onTap;

  ...
}

Becomes:

class _ColorPickerSwatch extends StatelessWidget {
  const _ColorPickerSwatch({
    this.color,
    this.selected,
    this.onTap,
  });

  final Color color;
  final bool selected;
  final Function? onTap;

  ...
}

If we also do primary constructors, it goes from:

class const _ColorPickerSwatch({
  required final Color color,
  required final bool selected,
  final Function? onTap,
}) extends StatelessWidget {
  ...
}

To:

class const _ColorPickerSwatch({
  final Color color,
  final bool selected,
  final Function? onTap,
}) extends StatelessWidget {
  ...
}

I'm generally not in favor of lashing two levers together a user might want to operate independently. That's why, for example, we have both final and sealed on classes because you might want to prevent subclassing independently of opting in to exhaustiveness checks.

But in this case, you can still operate the levers independently. You can have required nullable parameters and optional non-nullable ones. It's just that if the user doesn't pull the optional/required lever at all, we infer the completely obvious answer in places where we have the context to do so.

In places where there isn't that context, we make the user state it explicitly. This is arguably even better than the current behavior because when someone is reading a function type today, they might not realize that the language defaults to optional for all named parameters.

@munificent munificent added the feature Proposed language feature that solves one or more problems label Aug 17, 2023
@srawlins
Copy link
Member

Pair this with private named initializing formal parameters (#3058, #2509, ...), and we could save a lot of real estate!

I do think that the current required situation is a huge turn-off for new developers; it sucks to have to give a big speech about why required is required (and why fields can't be promoted), and I think it's important to address "huge turn-offs."

@srawlins
Copy link
Member

The behavior of abstract methods (and external and native?) is I believe not addressed above.

So given this code:

abstract class C {
  void m({int i});
}

class D implements C {
  @override
  void m({int i = 0}) {}
}

class E implements C {
  @override
  // ERROR: Invalid override.
  void m({required int i}) {}
}

Today, the requiredness is not explicitly specified on C.m, and it's inferred as "not required." And given the above change for this proposal,

Infer required for named parameters in function declarations that have potentially non-nullable types and no default value.

I think we would not change the behavior (not infer required) for abstract methods. In fact I think the proposed rules for typedefs should apply here, yes? Each named argument should be explicitly written as required or optional. Then class E above can be made legal: you can declare in a superclass that a named parameter is required, and each subclass must keep it required.

@lrhn
Copy link
Member

lrhn commented Aug 24, 2023

External and abstract method declarations are more, but not entirely, like function types than function declarations. So is redirecting factory constructors, which cannot have default values at all.

But they are written like declarations (positional parameters need names).
They're somewhere on the middle, whick makes it no big surprise that they don't fit into a hack which tries to treat function declarations differently than function types.

Maybe we need a third category, an "abstract function declaration", so that we can test the exceptions uniformly and separately when necessary.

@munificent
Copy link
Member Author

Ah, abstract is a good catch. Yes, we could require those to be explicit too and force you to write either optional or required.

@leafpetersen
Copy link
Member

I like the direction of this, but I have to admit I don't love that this is driven by the type, and even more so when the type is not immediately visible because of this parameters. From the original example:

  const _ColorPickerSwatch({
    required this.color,
    required this.selected,
    this.onTap,
  });

is at least very clear about what is and is not required, whereas with

  const _ColorPickerSwatch({
    this.color,
    this.selected,
    this.onTap,
  });

it is not at all clear to me which things are required and which things are not.

I think there was another proposal from someone (@munificent or @lrhn ?) to say that all parameters are required unless they have a default value (and I think to use a marker like int x= for an optional parameter in a function type). If we are going to consider a wholesale change to the function syntax, it would be interesting to compare this change with that one in terms of what it would look like and impact.

@munificent
Copy link
Member Author

I think there was another proposal from someone (@munificent or @lrhn ?) to say that all parameters are required unless they have a default value (and I think to use a marker like int x= for an optional parameter in a function type).

It's something I've noodled on but never put a full proposal together.

@mateusfccp
Copy link
Contributor

I think there was another proposal from someone

This is very similar to #878. Although the approach is not the same, the results are similar, IMO.

@munificent
Copy link
Member Author

This is very similar to #878. Although the approach is not the same, the results are similar, IMO.

Yes, thanks for bringing that up! The proposal here is basically an extension of that one that answers the question of how to make function types and abstract function declarations less confusing.

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

5 participants