language icon indicating copy to clipboard operation
language copied to clipboard

Allow setters to declare a return type of `Never`

Open mateusfccp opened this issue 1 year ago • 7 comments

This may be only useful on some few edge cases, so I am not sure if this is viable or not, but it may not be so hard to do and isn't breaking.

Currently, the specification explicitly states:

It is a compile-time error if a setter declares a return type other than void.

With null-safety and the Never type introduced, there are cases where one might want to change their setter return type to indicate that an error is always thrown.

For instance, UnmodifiableListView.operator []= always throw.

It could be useful to be able to declare the return type of those kinds of setters as Never.

mateusfccp avatar Dec 10 '24 20:12 mateusfccp

The return type of a setter doesn't matter, because the type isn't used anywhere in the specification. Any returned value is lost, so it's type doesn't matter.

If we allow Never as return type, as a signal that the setter invocation never returns any value, then we'd want the spec to recognize that. That would mean something like:

  • If an assignment expression invokes a setter (it's not an assignment to a local variable), then let R be the return type of the signature of the setter being invoked, and let S be the static type of the assigned expression. If R is a bottom type (any subtype of Never), then the static type of the assignment expression is Never. Otherwise the static type of the assignment expression is S.
  • It is be a compile time error if the declared return type of a setter is anything except void or Never. A setter declaration with no return type defaults to void (even if we can see that the cost definitely throws). An instance setter with no declared return type inherits a return type from any superinterface member signature as normal, and defaults to void if there is no superinterface setter.
  • It's a compile-time error if a concrete setter declaration has a bottom type as return type, and control flow can exit the setter body (reach the end, reach any return statement, or even contain any return statement, but this is just the same rules as for any method body with a bottom return type).
  • operator []= counts as a setter.

Could work. Not sure if it's worth the work, just to document that a setter throws. For a static setter, it just means that you should never call it, so it might as well not be there. This only makes sense for instance setters, but those also gave to be valid overrides, so any interface having a throwing setter implies that all subtypes must also throw. That again only makes sense for a sub-interface that disables supertype APIs, which isn't great OO design (breaks subtype substitutability). Can be useful occasionally, but not great.

lrhn avatar Dec 10 '24 23:12 lrhn

Yes, it would only be useful for instance setters, and only for a few cases. I feel like there are no downsides (except, obviously, for the resources spent to implement it), but maybe the benefits are not worthy enough.

I was thinking operator []= was already a setter (because it already doesn't allow anything but void). Does it have a different special case for the return type?

mateusfccp avatar Dec 11 '24 01:12 mateusfccp

Hi @mateusfccp

I have a quick idea to share! Currently, setters can only return void, but with null-safety and the Never type, it might be cool to allow setters to return Never in certain cases.

For example, UnmodifiableListView.operator []= always throws, so having a Never return type could clarify intent and enhance type safety.

What do you think?

Cheers, Sowmithri Sriram. sriramsowmithri9807 --> github

sriramsowmithri9807 avatar Dec 11 '24 05:12 sriramsowmithri9807

I was thinking operator []= was already a setter

I think it is, just wanted to be certain that it was included since "setter" sometimes means "something declared using set" and sometimes means "something behaving as an assignment". Here we want the latter.

lrhn avatar Dec 11 '24 14:12 lrhn

One workaround is to mark such setter with a @Deprecated('do not use'), like:

@Deprecated('The list is immutable, do not use');
@override
void operator[]=(int index);

rrousselGit avatar Dec 11 '24 15:12 rrousselGit

One workaround is to mark such setter with a @Deprecated('do not use'), like:

@Deprecated('The list is immutable, do not use');
@override
void operator[]=(int index);

This is an interesting workaround and may work, although the semantics conveyed are not correct.

mateusfccp avatar Dec 11 '24 17:12 mateusfccp

I agree that allowing setters to use Never as a return type would enhance clarity in cases like UnmodifiableListView.operator []=. It makes the intent explicit that such setters always throw and aligns well with null-safety principles. This seems like a reasonable and non-breaking improvement."

class UnmodifiableListView<T> {
  List<T> _items;

  UnmodifiableListView(this._items);

  Never operator []=(int index, T value) {
    throw UnsupportedError('Cannot modify an unmodifiable list.');
  }
}

void main() {
  var list = UnmodifiableListView<int>([1, 2, 3]);

  // Trying to assign a value will always throw an error
  list[0] = 10; // Throws UnsupportedError
}

sriramsowmithri9807 avatar Dec 12 '24 03:12 sriramsowmithri9807