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

Should we have generic getters? #1622

Open
eernstg opened this issue May 7, 2021 · 11 comments
Open

Should we have generic getters? #1622

eernstg opened this issue May 7, 2021 · 11 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@eernstg
Copy link
Member

eernstg commented May 7, 2021

Given that we plan to allow name<type, arguments> as an expression, do we then lift the restriction that getters cannot be generic?

This could be convenient, e.g., for the expression of "cast" getters:

extension AsExtension on Object? {
  X get as<X> => this as X;
  X? get asOrNull<X> {
    var self = this;
    return self is X ? self : null;
  }
}

extension AsSubtypeExtension<X> on X {
  Y get asSubtype<Y extends X> => this as Y;
}

void main() {
  num? n = 1 as dynamic;
  n.as<int>.isEven;
  n.asSubtype<int>.isEven;
  n.asOrNull<int>?.isEven;
}
@eernstg eernstg added the feature Proposed language feature that solves one or more problems label May 7, 2021
@lrhn
Copy link
Member

lrhn commented May 7, 2021

I'd say no. Not unless you can have generic fields.

The getter/field symmetry is the reason to have getters to begin with, otherwise they could just be nullary functions.
Having getter features which are not shared by fields is a non-goal to me.

That's a reason we don't already have generic getters. We could, like we have implicit instantiation of tear-offs or function calls, we could have had implicit instantiation of getter invocations if we wanted to.

I'm more interested in generic operators, because operators have actual arguments that could benefit from being parameterized by type. Like T operator+<T extends num>(T value) which, if used on int, would capture our current type-system special cases for int.+ (but would be hard to define as an override of num.+). Even without a way to write the type, it can act as an implicit existential type.

@Levi-Lesches
Copy link

On the semantics: Generics are referred to as "type arguments" because, just like regular arguments, they provide input to a function or type. To allow getters to have generics would be saying that getters can declare an input, which contradicts with distinction of getters and setters from regular functions.

@timnew
Copy link

timnew commented May 10, 2021

I would vote a safe cast operator rather than a generic getter, like as? in Kotlin, which represents asOrNull for short.
So developer can do the cast on use-site rather than on declaration-site.

@eernstg
Copy link
Member Author

eernstg commented May 10, 2021

@Levi-Lesches wrote:

To allow getters to have generics would be saying that getters can declare an
input, which contradicts with distinction of getters and setters from regular functions

True, a generic getter is a function from types to a value, so it does take an input. One consequence of this is that we probably won't have any stable generic getters. However, we're already considering the use of syntax like f<int> to denote a generic instantiation of a function f, so the notion that we can have a function from types to a value is already considered acceptable. There is no technical difficulty in making the distinction between a function that takes no input (a non-generic getter) and other functions.

It is true that a language construct like e as? T would be slightly more concise than e.asOrNull<T>, but the latter is a more general mechanism. For instance, we might want to use myList.whereType<T> rather than myList.whereType<T>(). How does that final () help anybody?

@Levi-Lesches
Copy link

There is no technical difficulty in making the distinction between a function that takes no input (a non-generic getter) and other functions.

For instance, we might want to use myList.whereType<T> rather than myList.whereType<T>(). How does that final () help anybody?

I think it's just the semantic distinction that has me on the fence. From Effective Dart:

Even so, choosing a getter over a method sends an important signal to the caller. The signal, roughly, is that the operation is “field-like”. The operation, at least in principle, could be implemented using a field, as far as the caller knows. That implies:

  • The operation does not take any arguments and returns a result.

Generics are not data input, but they still do represent an input, namely that the return value changes with the type argument used. Functions are typically the ones that return different values with different inputs.

@lrhn
Copy link
Member

lrhn commented May 10, 2021

If we want to allow you to omit parentheses from zero-argument function invocations, we can do so today. (We'd have to find a new syntax for tear-offs, but that's doable, say o::foo instead of o.foo. Probably much easier to parse).
(We can also allow o.foo = x; to invoke unary functions, as an alias for o.foo(x), and do away with getters and setters entirely!)

I don't see making whereType a "generic getter" as an improvement.

@joshualitt
Copy link

joshualitt commented Feb 8, 2023

I do appreciate the arguments against this feature on the grounds of semantic consistency, but nonetheless it is useful to have some syntax here. I ran into this exact issue with a CL I'm working on to convert between Dart and JS objects. Most of the JS objects map 1:1 to their Dart counterparts, but some conversion functions require a type argument, i.e. JSFunction <-> Function.

Now we're left in a situation where users have to write:

  edo = DartObject().toJS;
  obj = edo.toDart<DartObject>()

On the other hand, I definitely see @lrhn 's argument. I really like the suggestion of allowing users to omit parentheses from zero-argument functions, and TBH I actually kind of like :: for tearoffs because then it is fully disambiguated from a getter / field access, but changing the syntax of tear-offs seems like kind of a big lift.

Anyway, nothing concrete to add, other than this does occur, in real code, and hopefully some day we will have a solution here.

@eernstg
Copy link
Member Author

eernstg commented Feb 9, 2023

Thanks @joshualitt, real world examples are very important!


First, I'd like to retrace some of the arguments in this issue. I'm trying to understand the counterarguments, in particular the ones that we might paraphrase as 'getters should not receive any arguments (even type arguments) because then they aren't getters'. For instance:

@lrhn wrote:

The getter/field symmetry is the reason to have getters to begin with, otherwise they could just be nullary functions.

@Levi-Lesches wrote:

To allow getters to have generics would be saying that getters can declare an input, which contradicts with distinction of getters and setters from regular functions.

We can certainly say that they are functions/methods that receive one or more type arguments and no value arguments. Given that we do not have a problem with functions/methods that receive one or more value arguments, but no type arguments, why would it be a problem if we can support the converse? If it's really important to define these guys as functions/methods rather than getters then I'm sure we can come up with a method/function declaration syntax where it is indicated that this particular function does not take any parameters.

However, this would give rise to questions like 'why can't I tear off this method?'. It is probably going to work more consistently and conveniently if we do make them a variant of getters: x.myGenericGetter would then be able to receive actual type arguments by inference, rather than being a (not so useful) tear-off of a nullary method.

Hence, I'd prefer to support generic getters: There's nothing conceptually wrong with the notion of a function that receives type arguments, but not value arguments, and the technical realization of this concept as a generic getter has nice pragmatic properties.


Returning to the concrete case, let's consider an expression like edo.toDart<DartObject>.

We already have expressions like List<int> and myGenericFunction<int>. The former performs a computation that turns a generic class List into a type which is a particular generic instance of List, and the latter performs a computation (a generic function instantiation) where the type argument list <int> is passed to a generic function, again yielding a particular instance. Those computations are "small" and "built-in", but this shows that we do have prior art where this kind of syntax denotes a computation.

It is of course possible to make a getter perform a very expensive computation with side effects, but it's considered good style to ensure that a getter invocation can be understood as a query about an object property, it shouldn't change the state of the receiver (visibly), and invocations shouldn't be expensive. It shouldn't be any harder for the author of a generic getter to have this style rule in mind than it is for the author of any other getter.

A generic getter like edo.toDart<DartObject> could be considered to be a customizable version of a getter: We're asking for the value of the property toDart of edo, specifying that we want the <DartObject> variant of that property.

Also, DartObject obj = edo.toDart; could receive type arguments like <DartObject> by inference, which would make toDart a context sensitive getter. Doesn't seem crazy to me, and might even be useful. ;-)

@lrhn
Copy link
Member

lrhn commented Feb 9, 2023

There is no computation that can be done with a generic getter, which cannot be done with a generic zero-argument function, and the latter can also be torn off. It's all about syntax.

But syntax does matter, it'd all be lambda calculus otherwise.

If we introduce generic getters, aka functions which can be called without an argument list, but can still be given a type argument, then as @eernstg says, we probably can't tear them off, not if we also want to be able to infer type arguments. Their function type would probably be a normal function type with zero arguments, it's just invocation that's specialized.

Dynamic invocation gets tricky, but if we can do dynamicValue.genericMethod<int> to tear off an instantiated method, we should be able to handle dynamicValue.genericGetter<int> calling the getter.

Then we'd probably also want generic setters, which is trickier, but x.foo<int> = 42 can probably work.

@yanok
Copy link

yanok commented Feb 10, 2023

Nice to see there is some recent discussion on this.

This feature would make my life easier as Mockito maintainer. Currently Mockito has to override all methods of mocked class to accept nullable arguments since matchers like any return null. That confuses people a lot and leads to errors if people try to further extend the generated mock. I have a plan to get rid of these overrides, but that would require either changing the API from the getter any to the function any() (doable, but tons of code needs to be updated) or exactly this: generic getters.

I do understand though that Mockito just abuses getters to get a nicer syntax for expectations, so it's a "not so realistic" example and we definitely don't want to break the language for just this use case... but since there are others and in general the feature doesn't look that crazy, I'd vote for implementing it.

@eernstg
Copy link
Member Author

eernstg commented Feb 10, 2023

(@yanok, please do vote on the initial comment, it does matter ;-)

Of course, one could say that this is all about getting rid of a mere (), and who cares?

It might be useful simply because it's more concise. Otherwise, the given functionality could have been provided via a getter previously, but we'd now like to make it context sensitive because the underlying software (and language, actually!) has evolved, and it would cause a lot of churn to use a nullary method because we'd have to add all those ()s (like Mockito's any). But does it make sense?

From a language design perspective, and when it comes to code readability, comprehensibility, and maintainability, I think there are cases where these context sensitive getters are at least as readable as the current approach. Here's an example using the as and asOrNull declarations from the initial comment in this issue.

void f(int? i) {...}

void main() {
  num n = ...;
  var i = n.as<int>; // Could throw.
  var j = n.asOrNull<int>; // Won't throw, but is nullable.

  f(n.as); // Can use type inference to cast to exactly the type that's needed.
  f(n.asOrNull); // `f` accepts null, so we can avoid the potential throw.

  // Using extension methods rather than getters. Certainly possible, but is it better?
  f(n.as());
  f(n.asOrNull());

  // Relying on language primitives, no abstractions.
  f(n as int); // No diagnostics if `int` is too strict by mistake.
  f(n is int ? n : null); // The standard way to express `orNull`.
}

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