Skip to content

Revisit unimplemented factory constructors and default values #4172

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

Open
eernstg opened this issue Nov 25, 2024 · 18 comments
Open

Revisit unimplemented factory constructors and default values #4172

eernstg opened this issue Nov 25, 2024 · 18 comments
Labels
augmentations Issues related to the augmentations proposal. question Further information is requested

Comments

@eernstg
Copy link
Member

eernstg commented Nov 25, 2024

We need to introduce an extra rule about factory constructors that are subject to augmentation: They can omit default values.

With augmentations, a factory constructor can be unimplemented. It is possible to make a late choice about the nature of this constructor (it could be redirecting or it could be non-redirecting):

class A {
  A();
  factory A.foo(); // Unimplemented factory constructor.
}

augment class A {
  augment factory A.foo() = A;
}

class B {
  B();
  factory B.foo(); // Unimplemented factory constructor.
}

augment class B {
  augment factory B.foo() {
    return B();
  }
}

We may need to introduce an extra rule about these unimplemented factory constructors which says that it is allowed for an optional parameter to omit the default value clause, even in the case where the parameter type is potentially non-nullable.

For example:

class A {
  A([int i = 0]);
  factory A.foo([int i]); // Currently an error; this issue proposes to allow it.
}

augment class A {
  augment factory A.foo([int i]) = A; // Default value obtained from redirectee.
}

The general rules about augmentations state that no default value can be specified by an augmentation, it must always be specified by the introductory declaration (if there is to be a default value for that parameter at all).

This implies that the declaration above would not allow A.foo to be augmented with a class body (such that it is a non-redirecting factory constructor): The parameter type cannot be modified by an augmentation, and an augmentation cannot provide a default value. In other words, the fact that there is no default value has eliminated the expressive power to make the decision about the factory being redirecting or non-redirecting, it's always forced to be redirecting.

We could allow the unimplemented factory constructor to include the default value and then ignore it (because it would be an error to have it) when the constructor is augmented to be redirecting:

class A {
  A([int i = 0]);
  factory A.foo([int i = 1]); // Currently allowed.
}

augment class A {
  augment factory A.foo([int i]) = A; // Currently an error; this issue suggests we might allow it.
}

class B {
  B();
  factory B.foo([int i]); // Proposed: Allow this.
}

augment class B {
  augment factory B.foo([int i = 3]) { // We could allow this.
    return B();
  }
}

If we do not allow this then the choice to have a default value will force the constructor to be non-redirecting.

In other words, the ability to "decide late" whether a given factory is redirecting or not will disappear as soon as there is one or more optional parameters whose type is potentially non-nullable. Of course, we can then make the declarations inconsistent: If we have two such parameters and provide a default value for exactly one of them then no augmenting declarations can be written, because they will give rise to a compile-time error no matter what.

@dart-lang/language-team, WDYT? Are you willing to allow a default value to (1) occur in an augmenting declaration, and/or (2) be declared in the introductory declaration, but then erased in the final definition because tit would be an error?

@eernstg eernstg added question Further information is requested augmentations Issues related to the augmentations proposal. labels Nov 25, 2024
@jakemac53
Copy link
Contributor

jakemac53 commented Nov 25, 2024

My opinion is to not change anything here other than possibly augmenting the definition of "potentially redirecting" to indicate that the presence (or absence) of a default value on an optional, non-nullable parameter will determine the type of the constructor (as in redirecting or not).

It isn't intended to be a feature that an augmentation can alter this property of the constructor, this "potentially redirecting" definition is only there to describe how we handle the case where we can't know from the introductory declaration, not to try and add flexibility.

Edit: If we don't allow providing the default value but no body today, we should allow that.

@lrhn
Copy link
Member

lrhn commented Nov 26, 2024

I do want to introduce flexibility.

If a user writes a constructor signature, and adds a macro to it, I do want to give the macro the flexibility to choose the implementation.

That said, any number of details can lock the declaration onto being redirecting or not:

  • const implies factory (for now, I have an issue asking for const non-redirecting factory constructors.)
  • a default value implies non-redirecting.
  • a redirection or body is obvious.

It really is just the plain signature that is ambiguous.

@eernstg
Copy link
Member Author

eernstg commented Nov 26, 2024

One approach we could use would be to say that the default value can be specified in an otherwise pluripotent (sounds better than 'ambiguous' ;-) constructor declaration, and if it is an error to specify that default value in the resulting constructor definition then it will simply be ignored. We might want to maintain that it is an error if there are two conflicting default values in the same augmentation chain, or we could even allow that to enhance the overall flexibility. Example:

class A {
  A([int i = 0]);
  factory A.foo([int i = 1]); // OK.
}

augment class A {
  augment factory A.foo([int i]) = A; // OK, and the "inherited" default value is ignored.
}

class B {
  B([int i = 0]);
  factory B.foo([int i = 1]); // OK.
}

augment class B {
  augment factory B.foo([int i]) { // OK, and the default value is "inherited".
    return B(i);
  }
}

class C {
  C([int i = 0]);
  factory C.foo([int i = 1]); // OK.
}

augment class C {
  augment factory C.foo([int i = 2]) { // Error?
    return C(i);
  }
}

Taking one step back, I think we have the following main players when it comes to constructor augmentation:

graph LR
  subgraph Factory
    direction BT
    fnr["Non-redirecting<br>factory A(int i) => B(i);"] --> fp["Pluripotent<br>factory A(int i);"]
    fr["Redirecting<br>factory A(int i) = B;"] --> fp
  end
  subgraph Generative
    direction BT
    gnr["Non-redirecting<br>A(int i) {...}"] --> gp["Pluripotent<br>A(int i);"]
    gr["Redirecting<br>A(int i) : this(i + 1);"] --> gp
  end
Loading

This is already somewhat inhomogeneous because the pluripotent generative constructor is usable as such, but the pluripotent factory is a new invention (an 'unimplemented' factory), and it is an error unless there is an augmentation which will implement it (by redirection or by a body).

The fact that some of these arrows can be blocked by parameter default values makes the situation one more bit inhomogeneous.

@eernstg
Copy link
Member Author

eernstg commented Nov 26, 2024

Perhaps we should go a step further and simply say that it is not a supported feature to change the kind of constructor using augmentation? (It's a mess!)

So we would accept an introductory declaration like A(int i); (generative, non-redirecting, can add a body and/or an initializer list) or a introductory declaration like factory A(int); (factory, non-redirecting, can add a body), but they cannot be augmented into redirecting constructors. In short, the pluripotent forms are now always non-redirecting.

If you wish to declare a redirecting constructor in an augmentation chain then it just needs to be introduced as such. If there is no A(int i); in the augmented declaration then it is not an error to declare A(int i) : this.name(i + 1); in the augmenting declaration. Similarly for the redirecting factory.

I think this implies that there would not be any situations where a default value can be declared in the introductory declaration, and it becomes an error to have it in the augmenting declaration, or vice versa.

This takes away a little bit of expressive power, but it also gives rise to a more consistent (and less confusing) model.

graph LR
  subgraph Factory
    direction BT
    fnr["Non-redirecting<br>factory A(int i) => B(i);"] --> fp["Pluripotent<br>factory A(int i);"]
    fr["Redirecting<br>factory A(int i) = B;"]
  end
  subgraph Generative
    direction BT
    gnr["Non-redirecting<br>A(int i) {...}"] --> gp["Pluripotent<br>A(int i);"]
    gr["Redirecting<br>A(int i) : this(i + 1);"]
  end
Loading

@jakemac53
Copy link
Contributor

Perhaps we should go a step further and simply say that it is not a supported feature to change the kind of constructor using augmentation? (It's a mess!)

We should in general have a principle that we don't allow you to change any which could be introspected on, because that leads to different introspection results in different phases.

And, we probably do need to give you the ability to check if a constructor is redirecting or not, because it informs how you can augment the constructor.

Therefore, I think we are left with really only two reasonable options:

  • Seal the type of constructor based on its introductory declaration.
  • Expose the "pluripotent" property of constructors which could be redirecting or generative. This would have to be updated in between macros and would introduce unfortunate ordering constraints and reduced parallelism.

@lrhn
Copy link
Member

lrhn commented Dec 3, 2024

Expose the "pluripotent" property of constructors which could be redirecting or generative.

Could be .isNonRedirecting and .isRedirecting that both return false.

(Redirecting and generative are not mutually exclusive. We have almost all eight combinations of const or not, factory or generative, and redirecting and non-redirecting.)

Perhaps we should go a step further and simply say that it is not a supported feature to change the kind of constructor using augmentation? (It's a mess!)

Then it's not possible to augment a redirecting generative constructor, because it can only be redirecting if it has written a redirection, and we don't allow changing that redirection.
It's not possible to write a plain signature and allow someone else to decide the implementation, and let them choose whether that implementation is redirecting or not.

I would like that flexibility, to be able to write a signature and let a macro worry about the implementation, without my signature being taken as an implementation in itself.

Maybe allow abstract C(); as a pluripotent generative constructor, so you have something to write (and later augmentations can then add an implementation, removing the abstract. They have to, a constructor must not be abstract in the end.)

@munificent
Copy link
Member

Now that augmentations don't need to support every possible thing a macro could want to do, I think we have the opportunity to simplify a bit here. Here's a proposal:

  • A constructor like A(); or A() { ... } always declares a generative non-redirecting constructor. An augmentation can provide or add a body, but can't change it to be factory or redirecting.

  • A constructor like factory A(); or factory A() { ...} always declares a factory non-redirecting constructor. Again, an augmentation can provide or add a body, but can't change it to be generative or redirecting.

  • Redirecting constructors (generative or factory) can only be created by the initial declaration of a constructor. (The initial declaration may be inside a class augmentation, but it can't be a constructor augmentation.)

  • Redirecting constructors can't be augmented.

In short:

  • As usual, an augmentation can't change any user-visible attribute of a declaration. Once a constructor is generative/const/factory/whatever, it must stay that way.

  • Redirecting constructors and augmentations don't mix.

We could allow mixing them in a future language version if really compelling. But, honestly, I would much rather go in the direction of enhanced constructors (#4246), potentially const statements (#4261), and function forwarding (#3444). I think there's a path there that would lead us to not needing redirecting constructors at all. Simplifying constructors would be really nice.

What do you think?

@lrhn
Copy link
Member

lrhn commented Feb 12, 2025

I really think we should allow an "API declaration" that declares what the API properties of a member, including a constructor, should be, and let something else define the implementation.

Whether a constructor is const or not, or is factory or not, are API properties.
Whether it's redirecting is an implementation choice which is invisible in the API.

So I want to be able to write

@GenerateIt()
class A {
  A(int id);
  factory A.byName(String name);
}

and not lock a code generator into A and A.byName being non-redirecting.

I'd rather have the opportunity to allow the generator to decide the implementation strategy, and risk errors,
than just not having an option at all. It won't be an error then, unless the generator comes back and says "I won't run unless you make the constructor redirecting first". In that case, you win nothing other than having to write more text.

I prefer the variant of of augmentation where you can only add implementation once (like #4203) because it avoids a lot of ordering issues, and the entire augmented thing.
Then it's not an issue if one augmentation turns A() into a redirecting constructor and another tries to add a body, because it's just an error to try to add implementation twice.

We can also allow multiple augmentations that modify implementation, they just have to agree. If they don't, it's a compile-time error, and you should stop having two code generators modify the same thing if they can't agree.

If you end up with

class A {
  const A(int id);
  A copyWith({int? id});
}
// @<generated id="GenerateIt" v="1.1.5" t="20250213111902" h="0x25af397bc9">
class A {
  final int _id;
  final String _trace;
  const A(int id) : this._(id, "A.id($id)");
  A._(this._id, this._trace);
  A copyWith({int? id}) => A._(id ?? this._id, "$trace.copy(${id ?? ""})");
  String toString() => _trace;
}
// @</generated>
// @<generated id="other" ... >
class A {
  final int id;
  const A(this.id);
  int get hashCode => id.hashCode ^ 14f37;
  bool operator ==(Object other) => other is A && id == other.id;
}
// @</generated>

then we have a problem. There are two implementations of const A(int id), and they don't agree, not even on whether it's redirecting or not.
Neither uses augmented (but you shouldn't need to).

In this case we can layer these on top of each other and have a reasonable result:

  • First const A(this.id) runs to initialize id, then rather than calling super, it invokes const A(int id) : this._(id, "...");
  • That runs and redirects to A._(..).
  • Which runs and initializes _id and _trace and calls Object().
  • When this comes back it continues A._() (if it hadn't been const and had a body).
  • Then returns to the redirecting A(...) : this._(...)
  • Then returns to const A(this.id) (if it had had a body).
  • And it's done.

It's not necessarily an issue to have a non-redirecting generative constructor on top of a redirecting (it works if your super-cosntructor is redirecting too, it just takes more steps to get to the actual initializing constructor.)

It would be an issue if the generated parts were swapped, though, because a redirecting generative constructor
will not run the constructor it augments.
But that reduces the error-case to having two fragments that declare the same generative constructor, ordered such that a redirecting implementation augments a non-redirecting implementation.

Which really means that augmenting (generative) redirecting constructors is fine. We can do that easily.
It's augmenting with redirecting constructors that can be a problem, because it doesn't have a flow into the augmenting constructor.

Maybe(!) we can allow one redirecting constuctor too, if we think of it more like a super-constructor-invocation.
Your augmenting generative constructor fragments can include at most one super-constructor-invocation or
redirection. No matter where in the stack that super/redirection occurs, it's carried to the top and performed
after all the non-redirecting constructor initialization has happened. Then it jumps to the redirecting constructor
and continues, or to the super-constructor, and when it comes back from there, it runs the bodies as normal.
Probably too high risk of running initialization for the same fragment more than once.
(I tried this, it has issues. So does having redirection constructors only work on constructors in this or prior fragments.)

It's not easy. It might not be possible, but I really do want to try before saying that you can't augment or augment with redirecting constructors. (But I can see that they don't play well with augmentation, because they jump out of the augmentation chain.)

@munificent
Copy link
Member

I went ahead and scraped a pub corpus to see how common various kinds of constructors are:

-- Constructor (233346 total) --
 107272 ( 45.971%): generative                    ==============
  56758 ( 24.324%): factory                       ========
  51648 ( 22.134%): const generative              =======
  12332 (  5.285%): factory redirecting           ==
   3894 (  1.669%): const factory redirecting     =
   1222 (  0.524%): generative redirecting        =
    220 (  0.094%): const generative redirecting  =

From skimming the results, it looks like the majority (but not all) of factory redirecting and const factory redirecting constructors are coming from freezed generated code. That suggests to me redirecting constructors are useful for code generators and that that augmentations should play nice with them if possible.

The initial issue here isn't about augmenting constructors adding redirection in general (which the proposal supports). It's about how that interacts with default values. Say you've got a introductory constructor with an optional non-nullable parameter. The declaration may be factory or not and may specify a default value or not:

As it currently stands with the proposal:

  • Foo([int x]); Generative and no default value: Useless. The augmentation can't add a default value to make the parameter valid, nor can it turn the constructor into a redirecting factory constructor to make its absence valid.

  • Foo([int x = 0]); Generative and default value: Fine. The augmentation can add a generative body or a redirection and either works.

  • factory Foo([int x]); Factory and no default value: Half-stuck. The augmentation must turn it into a redirecting factory constructor and can't provide a body.

  • factory Foo([int x = 0]); Factory and default value: Half-stuck the other way. The augmentation must turn it into a non-directory factory by providing a body and can't turn it into a redirecting factory constructor.

That does not seem particularly flexible or elegant to me.

I propose that we take the notion of "can only complete once" and do something similar for default values: The introductory and augmenting declarations can omit or provide default values, but a given parameter can only receive a default value from one declaration (introductory or augmenting).

An augmenting constructor can choose to fill in a body or a redirection. Once that's all done, we see if the resulting declaration makes sense (i.e. has no default values if it's a redirecting factory, or does have default values otherwise).

Further, we remove the compile-time error that prevents a redirecting factory constructor from providing default values. Yes, if you write them, they will be dropped on the floor, so it lets users do something confusing and meaningless. But it doesn't seem any more confusing and meaningless to me than:

abstract class C {
  foo([int x = 1]);
}

We allow that and the world hasn't ended, so we can probably be a little looser in redirecting factory constructors too.

Some consequences:

  • Someone reading the introductory declaration may not be able to tell what the default value of a parameter is. This is equally true today when reading the declaration of a redirecting factory constructor or an abstract instance method. Not great, but not worse.

  • Augmentations have maximum flexibility to choose between a body or redirection for both factory and generative constructors, regardless of whether the introductory declaration provides default values or not.

How does that sound?

Thanks for catching this tricky corner case, Erik!

@eernstg
Copy link
Member Author

eernstg commented May 26, 2025

@munificent wrote:

The introductory and augmenting declarations can omit or provide default values, but a given parameter can only receive a default value from one declaration (introductory or augmenting).

Very nice analysis!

So we'd have the following relaxations:

  • An introductory factory constructor declaration with no redirection and no body can omit parameter default values where otherwise required (just like abstract instance methods).
  • An augmenting factory constructor with a body can declare parameter default values on optional parameters.

Moreover, the semantic factory constructor which is defined by an introductory factory and zero or more augmenting factory declarations must specify exactly one default value for each parameter which is optional and whose type is potentially non-nullable, and it must specify at most one default value for each parameter which is optional and whose type is nullable.

The tricky case is when a factory constructor has default values in the introductory declaration, and it is turned into a redirecting factory by an augmenting declaration. In this case we could allow it and ignore the default values (because they are not supported with redirecting factories). This is what the rule above implies. Alternatively, we would generalize redirecting factories to support default values that differ from the ones in the redirectee declaration (this implies that the redirecting factory would be a full-fledged function, not a notation which is fully eliminated at compile time; this is already true for the tear-off, anyway; see also #3427).

I don't think there is a need to change the rules for generative constructor declarations, so they can just specify their default values in the introductory declaration, as before, which is also what we do with all other kinds of functions.

We could allow "exactly one default value" to mean "one or more constant expressions whose values are identical" or "exactly one constant expression". I'd prefer the latter, because constant expressions with identical values may be easy to define and handle, but it doesn't generalize nicely to things like "you can specify one or more function bodies as long as they are equivalent" (that would be a canonical example of a Turing tarpit).

@lrhn
Copy link
Member

lrhn commented May 26, 2025

If we only allow one augmenting declaration to provide implementation, then I think we should just consider default values (like initializing formals and super parmeters) a part of implementation.
Only one declaration can add default values, the one that adds implementation. If a declaration does write a default value it is the implementation declaration.

We can loosen that to allow "documentation default values" which are not real, but exist only for documentation.
If a declaration has none of initializing formals, super parameters, an initializer list or redirection or a body,
then it can't possibly depend on the value of a default value.

We can then require a later actually-implementing augmenting declaration to declare the same default value, or redirect to something with the same default value, otherwise it's an error. Or we can leave it as something the analyzer can warn about.

So:

  • Don't allow default values except in the implementing declaration
  • Do allow default values elsewhere, and:
    • Require the implementing declaration to have the same default value (directly or by redirection).
    • Or not, the documenting default value was just, like, a suggestion.

I'd go with the first. If people really, really like to use default values for documentation, I'd go for the last.
If you want warnings when they don't match, it can be a lint.

@rrousselGit
Copy link

rrousselGit commented May 26, 2025

Hello there!

As I read this, a few things come to mind:

  1. Could non-abstract redirecting factories define default values too?
facctory Example([int a = 0]) = Redirecting;

Because if not, that makes this discussion pretty much useless for Freezed (as we want the = Redirecting, since that's how users customise the generated class name).

And given that Freezed is the main user of redirecting factories today, that'd feel odd to me :P

  1. Couldn't this be generalized to all abstract functions? Looking at you typedefs!
typedef Example = void Function([int a = 0]);
...

In terms of conflict resolution in cases of "What to do with the default / What if there's a default mismatch?"

IMO the only real answer is to completely ignore default values as part of function comparison (with maybe a warning) and treat it purely as a documentation thing.

One reason is: We can already override methods with default values, with a different default:

abstract class A {
  void fn([int value = 0]);
}

class B extends A {
  // Legal
  @override
  void fn([int value = 42]) {}
}

@munificent
Copy link
Member

If people really, really like to use default values for documentation, I'd go for the last.

Let's see...

Looking at individual constructor parameters:

-- Default (632449 total) --
 583895 ( 92.323%): No default   ============================================
  48554 (  7.677%): Has default  ====

And aggregating per constructor:

-- Constructor (233346 total) --
 217017 ( 93.002%): No default value parameters               =================
  16329 (  6.998%): Has at least one default value parameter  ==

So default values aren't very common in general.

It's redirecting constructors where this really matters, so just looking at those:

-- Redirecting generative constructor defaults (1442 total) --
   1365 ( 94.660%): No default value parameters               ==================
     77 (  5.340%): Has at least one default value parameter  =

(Obviously, 0% of redirecting factory constructors have default values since the language prohibits that.)

How common are redirecting constructors?

-- Constructor kind (233346 total) --
 158920 ( 68.105%): generative              =========================
  56758 ( 24.324%): factory                 =========
  16226 (  6.954%): redirecting factory     ===
   1442 (  0.618%): redirecting generative  =

So the only kind of redirecting constructors where you could provide a default value are quite rare today. Out of all 233,346 constructors measured, only 77 were redirecting constructors with any default values.

Overall, it seems like no, users don't really really like to use default values (for documentation or otherwise).

@munificent
Copy link
Member

If we only allow one augmenting declaration to provide implementation, then I think we should just consider default values (like initializing formals and super parmeters) a part of implementation. Only one declaration can add default values, the one that adds implementation. If a declaration does write a default value it is the implementation declaration.

I don't think that works out. Abstract methods can have default values, but we still want augmentations to be able to fill in bodies for them. I suppose we could say that an augmentation can only fill in a body for an abstract method if the method doesn't have any default values... but that feels pretty arbitrary.

The default values are already merely documentary in an abstract method (for better or worse), so it seems strange to treat them as so meaningful that they prevent an augmentation from adding a body.

I think maybe the easiest thing to do is to leave the proposal as it is.

It means augmenting constructors are limited in the ways that Erik points out but... users can just add or remove the default values in the introductory declaration if the limitations implied by them are a problem.

If you want an augmentation to be able to fill in your factory constructor with a redirecting body... then don't write any default values.

The situation might be different in a world with macros where users might want to be pretty oblivious to what the macro is doing to their code. But with code generators, there's a pretty tight coupling, and I think it's reasonable for a code generator to say "hey, I'm going to add a redirecting body to your factory constructor so no default values for you."

@rrousselGit
Copy link

Fwiw there's a non-zero amount of redirecting factories with default values in the wild.
Freezed has a syntax for it ; which was most likely not picked up by the tool

factory Class({@Default(0) int a}) = Redirecting;

@eernstg
Copy link
Member Author

eernstg commented May 28, 2025

@rrousselGit wrote:

Could non-abstract redirecting factories define default values too?

Certainly. This would be a new feature which would require the redirecting factory to be an actual entity rather than syntactic sugar for invocations of the redirectee.

We already need this for a tear-off, by the way:

class A {
  A();
  factory A.redir([int i]) = B;
}

class B implements A {
  B([int i = 1]);
}

void main() {
  print("Redirecting constructor: ${A.redir.runtimeType}"); // '([int]) => A'.
  print("Redirectee constructor : ${B.new.runtimeType}"); // '([int]) => B'.
}

The fact that those two function objects have (and should have!) different types show that the constructor A.redir is a separate constructor, not just syntactic sugar whose meaning is the constructor B.new.

The reason why redirecting factories do not support default value declarations on parameters is probably that these constructors weren't supposed to exist, they were just meant to be syntactic sugar. However, they do exist today, because of the semantics of constructor tear-offs.

It would simply be the natural next step in this evolution to introduce support for default values in the redirecting constructor declaration. We would need to allow the default values to be omitted, too (to avoid massive breakage), and the omitted default values would then be taken from the redirectee (preserving current behavior). This is consistent with the treatment of super parameters, so it would be in line with other parts of the language.

I still want to use the same rules as elsewhere, so I'd recommend that it is an error if the resulting default value is a compile time error:

abstract class A {
  factory A.redir([int x]) = B; // Error.
}

class B implements A {
  B([num x = 1.5]);
}

A.redir would be an error because A.redir([int x = 1.5]) ... is an error.

Alternatively, we could introduce a proper notion of forwarding and specify that the invocation of the redirectee will use the default values of the redirectee even in the case where this value would be a compile-time error if specified as the default value in the redirecting declaration. If we go this way then there would be a larger number of questions to resolve, but I would like to have support for proper forwarding in Dart, one way or the other.

So the overall answer is: Yes, the redirecting constructor could declare default values, but it takes a couple of language design steps before we're there.

Looking at you typedefs!

Adding default values to function types is a non-trivial step, too. We could make void Function([int _ = 1]) a subtype of void Function([int _]). It would also need to be supported by the backends: The run-time representation of a function type would then have to include the default values that are part of the type (which may be none, some, or all the default values of optional parameters in that type). Surely possible, but it isn't obvious to me that it would carry its weight.

We can already override methods with default values, with a different default

Right, that's another reason why I wouldn't want to add default values as a full-fledged element in function types. Also, if it's treated as documentation then it wouldn't need to be in the language, and it might presumably be handled via metadata.

@eernstg
Copy link
Member Author

eernstg commented May 28, 2025

Here's a proposal: #4393.

@lrhn
Copy link
Member

lrhn commented May 28, 2025

Abstract methods can have default values

And then they can't be augmented.I can live with that.

Default values is implementation, not signature.
You don't have to write a default value for an abstract function declaration. It means nothing arrogant, it's just a comment, a documentation of intent. (So are names of positional parameters.)

We can also allow an abstract declaration with default values, but then an implementing augmentation shouldn't have to use the same default value. It can, it likely wants to, but nothing prevents implementing the abstract method by adding a mixin as a superclass, which has a different default value. Why bother preventing it for an augmentation.

(I do think we should do the same thing for instance and static members and for constructors.)

I think I'd go with the latter: you can write default values if you want to, but unless it's the implementing declaration, they're just comments, and have absolutely no effect on anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
augmentations Issues related to the augmentations proposal. question Further information is requested
Projects
Development

No branches or pull requests

5 participants