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

Optionally named parameters #831

Open
eernstg opened this issue Feb 10, 2020 · 21 comments
Open

Optionally named parameters #831

eernstg opened this issue Feb 10, 2020 · 21 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@eernstg
Copy link
Member

eernstg commented Feb 10, 2020

This issue is a proposal to add support for parameters that may be passed as positional and also as named parameters. It includes support for passing named parameters at any position in a list of actual arguments.

[Edit: This proposal has been updated to take into account that named parameters can now be passed anywhere in a list of actual arguments.]

Motivation

One particular situation where this may be helpful is in Flutter code, where named parameters with the name child or children occur very frequently, and the code could be more readable if they can be passed as positional parameters:

Consider a build method like this one:

Widget build(BuildContext context) {
  return new Center(
    child: new Padding(
      padding: new EdgeInsets.all(10.0),
      child: const NameWidget()));
}

This could then be abbreviated as follows:

Widget build(BuildContext context) {
  return new Center(
    new Padding(
      padding: new EdgeInsets.all(10.0),
      const NameWidget()));
}

If it is considered desirable to have such a child or children argument at the end (such that the resulting code has a direct similarity to a tree structure, with some "small" property specifications at the beginning), and at the same time omit the name, it becomes necessary to allow positional arguments to occur after some named arguments. This feature has been added to Dart, version 2.17, known as "named arguments anywhere".

Grammar

The grammar is updated as follows:

<defaultNamedParameter> ::= // Modified rule.
    'required'? <normalFormalParameter> '?' ((':' | '=') <expression>)?

The name of a named formal parameter can be followed by a ?,
which indicates that this name may be omitted in invocations.

Static Analysis

When named actual arguments and positional actual arguments are mixed,
it is simply syntactic sugar for receiving all the positional ones first, and then the
named ones, albeit preserving the evaluation order. This is already part of Dart,
now that the 'named arguments anywhere' feature has been released.

We say that a named formal parameter is optionally named if its name is followed by ?.

An optionally named parameter may be passed as a positional argument. When
several optionally named parameters are passed as positional arguments, they are
associated with a name according to the order of declaration.

Consider a function invocation i of a function F where the statically known type of
F specifies k >= 0 positional parameters and m > 0 named parameters, of which
the parameters p1 .. pn are optionally named.

Assume that the invocation i passes k + n1 positional arguments to F, and it
passes named arguments q1 .. qn2.

It is a compile-time error if n1 > n. This would mean that the invocation passes
a larger number of "extra" positional arguments than there are optionally named
parameters.

It is a compile-time error if any name in q1 .. qn2 occurs in p1 .. pn1. This would mean that one or more named parameters are passed both as a
positional argument and as a named argument.

If i does not incur any errors then it is treated as the same invocation, except that exactly k arguments are passed as positional arguments, and the remaining n1 positional arguments are passed as named arguments, such that the names are selected according to the order of declaration of optionally named parameters in the type of F; finally, the named arguments are passed as specified in i.

For example:

void foo(p1, p2, {p3?, p4?, p5}) {}

X show<X>(X x) {
  print(x);
  return x;
}

main() {
  // Evaluate arguments in textual order, then pass argument list
  //  `("a1", "a2", p3: "a3", p5: "a5", p4: "a4")`.
  foo(p5: show("a5"), show("a1"), show("a2"), show("a3), p4: show("a4"));
}

The resulting invocation is subject to the same static analysis as an invocation in the pre-feature language using the invocation which is the result of the desugaring. For instance, we would get a compile-time error in the example above if p3 had had type int, because the actual argument has static type String.

The property of being optionally named is included as a part of function types, as well as the declaration order for optionally named parameters.

Subtyping among function types exists as currently specified, with the following extra constraint:

The sequence of optionally named parameter names of a function type F1 must be a prefix of the sequence of optionally named parameter names of a function type F2, or F2 <: F1 does not hold. If the static type is F1 and it justifies passing n1 optionally named parameters in a specific order as positional arguments, then it must be the case that the subtype which is the actual type of the callee actually accepts each of them, and assigns them to the same named parameter. So the subtype must have the same names, in the same order, and with no extra names in between. But the subtype could declare additional optionally named parameters, and they could be fresh, or they could be among the ones that are already declared in the supertype, but not optionally named.

For example, int Function(int i, {num j?, num k?}) is a subtype of num Function(int i, {int j?, double k}), but it is not a subtype of int Function(int i, {num k?, num j?}).

Dynamic semantics

The execution of programs containing optionally named parameters is fully determined by the steps described in the section about the static analysis.

That is, this mechanism is just syntactic sugar, assuming that we can use a let mechanism to obtain the correct evaluation order.

Note that this means that there is no support for passing named arguments as positional arguments in a dynamic function invocation. In this case they are simply passed as positional arguments, and a dynamic error occurs if the callee does not accept the given number of positional arguments.

Discussion

It would surely be possible to adjust the dynamic function invocation mechanism in such a way that optionally named parameters could be detected and re-routed to the correct named parameter. This would allow the language to have a more uniform semantics.

Note, though, that we already have some invocations that are "statically typed only": A dynamic invocation can never invoke a static extension method. Other situations where the static type of an expression may change the dynamic semantics of evaluating that expression arise in many cases: Evaluation of 42 may yield an instance of double because of the context type; a type argument may be inferred and reified by the callee when a generic function is called; or a list literal or an instance creation is evaluated, and the resulting type arguments are made part of the new object. So Dart already allows the dynamic semantics to be affected by the static type of an expression in many cases.

We could restrict the number of optionally named parameters to at most one in any given function type or member signature. This would eliminate the reliance on the textual order in the declarations of named parameters. However, that seems to be overly pure, because we already rely on the textual order for normal positional parameters (required as well as optional), and the ability to be passed by position makes it unsurprising that the ordering of optionally named parameters is significant.

We could require that the optionally named parameters are declared first in the {...} where all named parameters are declared. We leave this as a matter of style, because the semantics is well-defined even when this rule is violated. But a lint might be used to maintain that style, if desired; after all, the optionally named parameters will then be as close as possible to the position where they can be passed positionally, almost as if each of them can jump to the left over the {, and get appended to the list of positional parameters.

Optionally named parameters interact with the required modifier that a named parameter can have. However, we do not see the need to make this combination an error: If any given optionally named parameter is required then it must be provided in every invocation, but it may be provided positionally as well as by name. The former may be convenient in case all the earlier optionally named parameters are already being passed positionally, and the latter will allow the required parameter to be passed even in the case where it is not possible to pass it positionally (because some earlier ones are not being passed, or we do not wish to pass them positionally at this call site).

This mechanism would interact with another language mechanism which has been under consideration for a long time: Supporting formal parameter lists containing both optional positional parameters and named parameters.

If we introduce that feature then we would need a disambiguation mechanism: If an invocation of a function accepting k required positional parameters passes k + n1 positional actual arguments, then some of them could be intended to be optional positional, and others could be intended to be optionally named. Given that the optionally named ones can always be passed with a name, we'd recommend that each such positional argument is considered optional positional until the list of optional positional parameters has been exhausted, and the remaining positional parameters are bound to optionally named parameters in the given order. This is presumably not very convenient or elegant, but it is well-defined. Moreover, it could be considered bad style to mix the two kinds of parameters.

@eernstg eernstg added the feature Proposed language feature that solves one or more problems label Feb 10, 2020
@Cat-sushi
Copy link

We say that a formal named parameter is optionally named if its name is followed by ?.

What the relation with #156?
Does this proposal make required for non-nullable named parameter optional?

@Cat-sushi
Copy link

I've understood that It is name that optional thing is.

@Cat-sushi
Copy link

@munificent has another idea for omitting child/children.
ui-as-code/parameter-freedom.md at master · munificent/ui-as-code

@Cat-sushi
Copy link

Discussions are here, munificent/ui-as-code#15

@eernstg
Copy link
Member Author

eernstg commented Feb 11, 2020

@Cat-sushi wrote:

What the relation with #156?

#156 is about the conflict that arose because named parameters used to be optional (we had @required, but not as part of the language, in particular, not as part of a function type), but with nnbd they might not have a default default value because null would only work for some types. So we added support for required as part of the declaration of a named parameter.

This proposal is orthogonal. Some hints are given in the discussion section, but the point is that we can handle all combinations. For instance:

void foo(int i, {bool p1? = false, required bool p2}) {}

void main() {
  foo(1); // Error: `p2` must be provided.
  foo(1, p2: true); // OK, we can provide it by name.
  foo(1, true, true); // OK, like `foo(1, p1 = true, p2 = true)`.
  foo(1, true); // Error, `p2` must be provided, but `true` here means `p1: true`.
}

@munificent has another idea for omitting child/children.

Indeed, we have considered several different models.

This one is directly covered by this proposal. This proposal also allows declaring child/children as a required positional argument and passing it after some named arguments. However, that would require all call sites to remove child: and children:, whereas this proposal would allow the two styles of invocation to coexist.

This one allows both positional and named parameters (also an old topic in the Dart language team ;-). I think that solution would be slightly less convenient. For instance, if the child/children argument can be provided both as a named argument (just a regular one with child:, children:, not optionally named), and it can also be passed as an optional positional argument, how would you choose what to do if a call site provides both? Would you introduce a notion of "this parameter and that parameter is actually the same parameter, and it's an error if we pass both of them"?

Rest parameters involve a mechanism which is at times known as varargs in other languages. This would allow us to pass a bunch of children without the enclosing [ ... ], which might be desirable (or maybe it's not so good for the overall readability). In any case, I believe that this kind of parameter would generally be the last positional parameter. I find it likely that we could still add support for such a mechanism, if we want to have it, and that mechanism would blend in with a mechanism like the one proposed in this issue. It basically means that we rely on the statically typed invocation, and starting from a specific position we just gather all the arguments into a list whose element type is determined by the function type, and then the callee will receive that list as the last positional argument. I consider rest parameters to be interesting in this context, but orthogonal to the proposal of this issue.

The notion that a spread can be provided as an actual argument works well in the context where that part of the argument list is already recognized as a rest argument: We know which type of list is expected, and it is not hard to insert sublists, or to apply static checks on them. However, it gets more fuzzy in some cases, and I'm not immediately convinced that we can allow spread arguments in a typed setting where each parameter is an actual argument for a different formal parameter, that would be much more subtle (and presumably error-prone) to handle than the case where every spread is just contributing a sublist to a rest parameter.

By the way, if we prefer a style where the language doesn't include support for rest parameters, and those parts of the actual argument list will be given explicitly as lists, then we already have the spread operator when that list is a list literal.

@eernstg
Copy link
Member Author

eernstg commented Feb 11, 2020

About bind (cf. flutter/flutter#47793): I don't think it would be very hard to introduce support for an operation on an object whose type is a function type, which would just be syntactic sugar for currying:

void foo(int i, int j, {int k = 12}) {
  print("$i, $j, $k");
}

main() {
  var f = foo.bind(1); // `f` has type `void Function(int, {int k})`.
  f(2, k: 3); // Prints '1 2 3'.
}

Such a bind method could be added on all function types, but it would not be supported to tear it off or in any other way obtain a dynamic representation of it. The semantics would simply be to replace the invocation of bind by a function literal that accepts the remaining arguments and uses the static type of the receiver to declare parameters for that; the function literal would invoke the receiver, passing the result of evaluating the specified curried arguments, and also passing the remaining arguments.

The tricky parts would be how to handle default values (a function type does not include information about default values, so we might want to restrict the mechanism to statically resolved functions rather than all objects whose type is a function type), but I'm sure we could sort that out.

The remaining issue is just that this is (also) a non-trivial amount of work, and we need to have a sufficient amount of support for doing it..

@Cat-sushi
Copy link

I like this proposal from the view point of migration of Flutter framework.

I consider rest parameters to be interesting in this context, but orthogonal to the proposal of this issue.

Exactly.

Anyway I like the idea of rest parameter for children, because it reduces nest/ indent of Flutter code.

@eernstg
Copy link
Member Author

eernstg commented Feb 11, 2020

@tatumizer wrote:

This argument applies to every new feature, including the current one :-)

Certainly!

you are facing an argument that "child" and "children" add a lot of value

That discussion may have developed slightly over time.

"optionally named parameters" will work better in tandem with "bind".

That wasn't so obvious to me. Could you give an example?

@ds84182
Copy link

ds84182 commented Feb 11, 2020

@tatumizer

With bind, readability sure does suffer :)

If you split your widgets at semantic/style boundaries, it becomes much easier to follow from first glance:

class MyAppBar extends StatelessWidget {
  final Widget title;
  final List<Widget> actions;
  const MyAppBar({Key key, this.title, this.actions}) : super(key: key);

  Widget build(BuildContext context) {
    return Container(
      height: 56,
      padding: EdgeInsets.symmetric(horizontal: 8),
      decoration: BoxDecoration(color: Colors.blue[500]),
      child: Row(
        children: [
          MenuIcon(),
          Expanded(child: title),
          ...actions,
        ],
      ),
    );
  }
}

No need for bindings here.

Then your standard primary app bar (since some pages might use their own special ones):

class PrimaryAppBar extends StatelessWidget {
  final Widget title;

  const PrimaryAppBar({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MyAppBar(
      title: title,
      actions: [SearchAction()],
    );
  }
}

etc.

Binding is a bandaid fix to extremely nested widget trees. Properly using the widget abstraction can help your code readability, just like how declaring functions and using them instead of inlining/repeating the same code snippet can help with readability.


@eernstg

An interesting idea, but how does it interact with the combination of optional positional parameters and named parameters (if we wanted to allow this)? Does it essentially count the positional arity separate from the named parameters?

Either way, this reminds me of Lua tables, where positional values and keyed values can be intermixed: {1, a = 'a', 2}.

@eernstg
Copy link
Member Author

eernstg commented Feb 13, 2020

@tatumizer wrote:

Here's an example of how bind, working together with optionally
named parameters, may help

Thanks! I was thinking about currying, and hence bind, as a mechanism for concisely specifying function specialization (so you have a function with 5 arguments, pass 2 of them, and get a new function that takes 3 arguments; it is "specialized" in the sense that it has already made a specific choice about the first 2 arguments). If you're using that to pass (+ 2) to map in a functional language then it's an algorithmic device, whereas your example puts the emphasis on the organization of the code. But, of course, there's nothing stopping us if we want to run Container.bind(...) to get a function where some properties are fixed, and then pass that function to some algorithm where it is called several or many times.

In any case, I think currying is certainly a well-known and useful concept. There will be some considerations about the performance, because it does take some time to create extra function objects, and first class function objects are less accessible to static analysis than direct invocations (say, in order to inline a function), but that's true for many different abstractions.

@ds84182 wrote:

how does it interact with the combination of optional
positional parameters and named parameters

I wrote a few words about that in the 'Discussion' section. I think the combination of optionally named parameters and support for optional positional parameters and named parameters together could be simple. Everything would be well-defined, but in practice you might not want to use both of them together.

If we are calling a function f whose type has k required positional parameters, n optional positional parameters, and m optionally named parameters p1 .. pm, then if we consider a call with k+1 actual arguments, all unnamed, then we need to disambiguate. Since the optionally named parameters can always be given in a named manner, I think we'll have to say that the optional positional parameters must be received first, all of them, and then any extra positional parameters (after the k + n first positional ones) will be bound to optionally named parameters following the usual rules. For example:

void foo(a1, a2, [a3, a4], {p5?, p6}) {}

void main() {
  foo(1); // Error, too few positional parameters.
  foo(1, 2); // OK, binds a1:1, a2:2.
  foo(1, 2, 3, 4, 5); // OK, binds a1:1, a2:2, a3:3, a4:4, p5:5.
  foo(1, 2, p6:6, 3, 4, 5); // OK, binds a1:1, a2:2, a3:3, a4:4, p5:5, p6:6.
}

@eernstg
Copy link
Member Author

eernstg commented Feb 14, 2020

this part can be memoized

Right, the trade-offs are complex.

@igotfr
Copy link

igotfr commented Apr 9, 2020

void foo(p1, p2, [p3, p4], {p5?, p6}) {}

foo( 1, 2, (3 or null), (4 or null), (5 or p5:5 or null), (p6:6 or null) )

// apply combinatory
foo( 1, 2 ) // binds p1, p2
foo( 1, 2, 3 ) // binds p1, p2, p3
foo( 1, 2, 3, 4 ) // binds p1, p2, p3, p4
-> foo( 1, 2, 3, 4, 5 ) // binds p1, p2, p3, p4, p5
-> foo( 1, 2, 3, 4, 5, p6:6 ) // binds p1, p2, p3, p4, p5
foo( 1, 2, p5:5 ) // binds p1, p2, p5

if I use @required on p5, I will mandatorily to assign p3 and p4

void foo(p1, p2, [p3, p4], {@required p5?, p6}) {}
f(1, 2, 3); // error: p5 must be initializad

@igotfr
Copy link

igotfr commented Apr 9, 2020

void foo(p1, p2, [p3?, p4], {p5, p6}) {}

foo( 1, 2, (3 or p3:3 or null), (4 or null), (p5:5 or null), (p6:6 or null) ) {}

// apply combinatory
foo( 1, 2 ) // binds p1, p2
-> foo( 1, 2, 3 ) // binds p1, p2, p3
-> foo( 1, 2, 3, 4 ) // binds p1, p2, p3, p4
-> foo( 1, 2, 3, 4, p5:5 ) // binds p1, p2, p3, p4, p5
-> foo( 1, 2, 3, 4, p5:5, p6:6 ) // binds p1, p2, p3, p4, p5
foo( 1, 2, p5:5 ) // binds p1, p2, p5

if I use @required on p3

void foo( p1, p2, [@required p3?, p4], {p5, p6} ) {}
f(1, 2, 3); // ok

if I use ? on p4

void foo( p1, p2, [p3, p4?], {p5, p6} ) {}
f(1, 2, 3); // ok
-> f(1, 2, 3, 4); // ok

if I use @required on p4, p3 will need mandatorily to be initialized

void foo( p1, p2, [p3, @required p4?], {p5, p6} ) {}
f(1, 2, 3); // error: p4 must be initialized
f(1, 2, 3, 4); // ok

@eernstg
Copy link
Member Author

eernstg commented Apr 9, 2020

@cindRoberta wrote:

if I use @required on p5, I will mandatorily to assign p3 and p4

We used to have some support for @required on named parameters, but that mechanism is obsolete. It wasn't a language mechanism, and in particular there was no support for it in the type system. However, with the upcoming null-safety feature we will instead get support for a proper language mechanism where a named parameter can have the modifier required, and this is checked statically.

(During the transition to a world where every library enables null-safety the static checks can be subverted by legacy code, but it is a strict and statically checked mechanism in the pure null-safety world.)

Note that required cannot be used on optional positional parameters. It would have to be a prefix of the parameters declared in the [...] block anyway (you can't make the 2nd parameter in that block required without also making the first one required), so you just need to move the [ to the right, thus turning some of the optional positional parameters into regular (in particular: required) positional parameters. So I won't speculate about how required would work on positional parameters: That won't happen anyway.

So let's consider the modifier required and the interactions with optionally named parameters:

I think the best approach will be to keep the rules about the binding of actual arguments to formal parameters unchanged. This means that any given invocation will bind the given arguments to certain formal parameters, and then it's simply an error if a required parameter exists and hasn't been provided.

So here's the example:

void main() {
  void foo(a1, a2, [a3, a4], {required p5?, p6}) {}
  f(1, 2, 3, 4, 5); // OK, a1:1, a2:2, a3:3, a4:4, p5:5.
  f(p5: 5, 1, 2); // OK: a1:1, a2:2, p5:5
  f(1, 2, 3); // Error: Required named parameter `p5` not provided.
}

@escamoteur
Copy link

I guess this discussion went a bit off the original topic?
Indeed I still would like to be able to omit child and children and this would be an elegant way to stay backward compatible

@eernstg
Copy link
Member Author

eernstg commented May 31, 2023

I guess this discussion went a bit off the original topic?

They do. ;-)

I just updated the proposal to reflect the fact that named-arguments-anywhere has been added to the language. This simplifies several parts of the description because it doesn't have to say anything about the 'anywhere' feature.

@TimWhiting
Copy link

TimWhiting commented Jun 1, 2023

As I understand it the main disadvantage of this approach would be not supporting dynamic invocations. I think that is a reasonable tradeoff. Dynamic invocations are inherently unstable --> no indication that if you change a definition it will cause a runtime exception. So most dart code stays away from them for the most part, and new dart code written after this feature is probably just as likely if not more likely to stay away from dynamic invocations.

Dynamic invocations are only useful when the type system is not flexible enough to prove some invariants, and in those cases the user takes responsibility away from the language to ensure that the invariants are upheld. As such, apis involving those user responsible invariants tend to be small and highly predictable. Keeping to a smaller portion of the language that was available before this feature seems fine to me (as someone who has had the need to use dynamic invocations occasionally).

The benefit on the other hand is huge if flutter can change children / child to be optionally named which would remove a huge amount of nesting, and arguments over the limited line-length.

@eernstg
Copy link
Member Author

eernstg commented Jun 1, 2023

@TimWhiting wrote:

if flutter can change children / child to be optionally named which would remove a huge amount of nesting

Interesting! I hadn't thought about that, how would that happen? Here is the original example where we assume that child and children are optionally named. First the standard invocation, directly from flutter sources, but without the news (to get a non-verbose form already):

Widget build(BuildContext context) {
  return Center(
    child: Padding(
      padding: EdgeInsets.all(10.0),
      child: const NameWidget()));
}

Then the version where child is passed positionally:

Widget build(BuildContext context) {
  return Center(
    Padding(
      padding: EdgeInsets.all(10.0),
      const NameWidget()));
}

I can see that several lines get shorter because we're deleting text (child: simply goes away), but I would assume that those lines aren't that long in the first place. It would often be a class name, perhaps with an import prefix, plus possibly some type arguments, and then (.

Anyway, I do agree that the overall visual impression is less noisy.

@eernstg
Copy link
Member Author

eernstg commented Jun 1, 2023

By the way:

Dynamic invocations are inherently unstable

True, they will break at run time (which is much more difficult to fix than a compile-time error), and we won't get any hints at all about the need to reinspect them when something that they depend on has changed.

However, dynamic invocations serve a different purpose as well, an almost philosophical purpose: In the situation where an expression e evaluates in a particular way ("look up that variable, call this function with that argument, etc. etc."), it may be useful to ask whether or not the expression e1 which is obtained from e by making all operations dynamic has the same behavior (e.g., if e is 1.isEven then e1 could be (1 as dynamic).isEven).

If they have exactly the same behavior then we have just established that the semantics of the expression can be understood (correctly) no matter which types e and all of its subexpressions have.

The opposite end of the spectrum occurs when the evaluation of a given expression depends in essential ways on the static typing of e and its subexpressions.

In the former case we may rely on a lot of static type checking to ensure that the expression is consistent with the given library/program where it occurs. However, the type checks don't "do" anything, they're just there to complain if anything looks wrong.

In the latter case the typing influences the meaning of the program. For instance, we might call one extension method rather than another one based on the precise type of the receiver, we might create a List<int> or a List<Widget> based on the context type around the expression [], and so on and so on.

We have lots of significant effects of typing in Dart. This is often extremely convenient. On the other hand, it implies that a reader of the code may basically need to run the type checker in their head in order to (really) understand what the program is doing. This is the reason why I'm keeping an eye on the "type independent" semantics, whenever we have some of that.

So when I'm saying that it is somewhat worrying that this particular mechanism will definitely not work in a dynamic invocation, I'm basically saying that "this mechanism may be smart and tempting, but we should remember that it is also a tax on the reader of the code, because they need to keep track of a larger amount of static type information in order to comprehend when it's used, and how".

@TimWhiting
Copy link

Sorry, I meant indentation and not nesting. Using flutter there is often a balance between splitting components of a screen into separate widgets or keeping them in a single widget. Modularity is useful when you need to use a widget in more than one place, however often it is encouraged to split out smaller components when nesting gets deep due to visual issues and line wrapping - even if that component is used only in one place. This causes more cognitive overhead for understanding a full visual layout due to not having the full code for the layout in one file or having to scroll back and forth to edit what is conceptually closely tied to each other.

Anyways, I think that visually / cognitively this issue could have a large impact on the ecosystem. Probably more than named arguments anywhere (due to flutter's reluctance of converting the child parameter to positional - which they declined to do due to breaking changes and not always wanting positional everywhere, or positional ordering issues). Ideally named versus positional usage should be the user's choice which this proposal gives more credence to.

@TimWhiting
Copy link

So when I'm saying that it is somewhat worrying that this particular mechanism will definitely not work in a dynamic invocation, I'm basically saying that "this mechanism may be smart and tempting, but we should remember that it is also a tax on the reader of the code, because they need to keep track of a larger amount of static type information in order to comprehend when it's used, and how".

I understand the implications here. However, with good language server support and editors that provide inlay hints or inferred types on hover this becomes less of an issue, and as mentioned previously extension methods already introduce this need for the user to keep track of static / inferred types. With possible static extension methods, and inline types, I don't see the need for the user to keep type information in their head or in their editor going away. In cases where it becomes non-obvious which method a particular invocation resolves to a reviewer would probably ask the code author to add static types to local variables.

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

6 participants