-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Support generic arguments to operators #30048
Comments
The language doesn't support generic arguments to operators. There is no good place (syntactically) to put the arguments. In some places, as in your examples, the type inference could infer the types and insert them for you, but there are cases, where that's not possible, or where the user wants to pick a different type. In that case there wouldn't be any way to fix the type. Edit: if operators had a different way to call them (say |
Any workaround for this? |
not really. You either need to use a function, or make the types less strong. |
with less strong types - won't be auto inference. With function it'll be as hard to read as direct function invocation. 😞 |
I agree that fewer types only work in some places (and probably not yours). A direct function call is ok, but there are advantages to member functions: they can be nicely code-completed. It's, obviously, up to you to decide if it's worth here. To be clear: I can see how your code could benefit from generic operators, and we are going to think about this use-case in the future. In particular, we would consider it, when we discuss different syntactical accesses to operators (as mentioned in my example above). These could come up in other situation, like tear-offs. At the moment, we are not working on these, though. |
Ok. I see. Thanks. |
Has there been any additional thoughts or plans on this point? I think I'd like to have type inference on the generic operator and no way to be explicit about the generic (or maybe just an awkward syntax) as opposed to no generics on operators at all? I have some code whereby the type inference would always work and the syntax would look nice (of course, I don't know how possible this would be though) |
There is no reasonable way to add generics to operators, because there is no way to provide the type argument when using the operator. |
Could it work if the type was inferred? If you can't infer the type then just set as dynamic. |
@twistedinferno given that types are reified in Dart that would be pretty shady: the inference algorithm would have to be part of the spec in order to derive the semantics of such a program. |
@polux could it be added to the spec, maybe kept open as a possible in the future? (I thought type inference was quite good in Dart2 anyway, I think it would already work in a lot of situations I'm thinking without any changes but I could be missing something as I've only been playing around with the language for a short while).
|
@twistedinferno my point is that if you default to dynamic when the type can't be inferred, and the body of your operator inspects the type argument it is passed, then the behavior of your program depends on whether type inference succeeded or not. The alternative is to fail type checking when the inference fails, but without syntactic support for explicitly passing type arguments this is not acceptable. |
does it not already default to dynamic when you define a function without a type? It defaults to dynamic in the situation like below and then anywhere we use the result from the function there will be no type checking.
|
ah, sorry, I think I understand what you mean. That example isn't right, it is not generic! |
|
|
What I mean is something like this:
Let's say |
Many thanks for clarifying and thinking more on the topic, the type inference is clever than it seems :-) My editor in vscode shows variables of type dynamic but when the program is compiled Dart seems to assign a type if it needs to. I ran this example trying to trick the compiler but it shows a runtime error when I uncomment the print value line. Very interesting and thanks for your time :-)
|
I think I'll add some words to this thread, just to clarify a few things that do not seem to be self-evident. ;-) @polux wrote
Already simple stuff like @twistedinferno wrote
var fn = <T1>(T1 x) => x;
fn(5) is int; //true What happens here is that you are testing that the result returned by var fn = <T1>(T1 x) => x;
main() {
print(fn<Object>(5) is int); // Prints 'true'.
} But the inferred type argument will actually have the value var fn = <T1>(T1 x) {
print(T1);
return x;
};
X fnNamed<X>(X x) => x;
main() {
fn(5); // Prints 'int'.
print(fn.runtimeType); // Prints '<T1>(T1) => T1'.
int Function(int) fnInt = fnNamed;
print(fnInt.runtimeType); // Prints '(int) => int'.
print(fnInt(5)); // Prints '5'.
} You can also inspect the type of So there are a lot of situations where inference takes place, and matters. So when you test the following it doesn't mean that there were no types: print(result is dynamic); // true It just means that you have confirmed that @polux wrote
f<A>(A x) {
return (A == int) ? 1 : 2;
}
But that's certainly also the case: f<X>(X x) {
return (X == int) ? 1 : 2;
}
main() {
print(f(42) == 1); // 'true'.
print(f("Hello!") == 2); // 'true'.
print(f<Object>(42) == 2); // 'true'.
} But the features that you are relying on here are (1) reification of type arguments (such that there is a representation of them at run time), and (2) type inference (implicitly providing some type arguments). So I think you can get all the things you are referring to, with regular methods. But this issue had a different focus from the outset, namely syntax: An operator is no different from any other instance method, except for syntax, and that's exactly the reason why it is not trivial to enable invocations of operators to take type arguments explicitly, and the only reason why we don't support generic operators is the syntax. So when you want a generic operator you can always work around it by writing a regular generic instance method and calling that. You get everything you've requested except the operator syntax. |
Thanks, I had not realized that was already the case with "regular" methods. I assumed that in case the type inference of the type argument of a function failed, the program would be rejected. Even if that was the case, your example indeed demonstrates that the program's behavior depends on the result of the type inference, even when it succeeds. |
But that is indeed the case: void f<X extends int>(X x) {}
main() {
f("Splut!"); // Compile-time error.
} And the run-time behavior of a program with no errors certainly depends on the outcome of type inference, in various ways, especially because type arguments are reified. |
Ok. To me that was type checking because it doesn't require unification but I guess that's still type inference indeed. |
What I was getting at is: there are cases where Java's or Haskell type inference give up not because of a type mismatch but because there aren't enough constraints to infer a type and/or the inference algorithm is not smart enough to generalize in the right place. In these cases these languages don't fall back on "dynamic". |
many thanks for explaining so well @eernstg (and for reminding me that
Going back to the operators, the problem with the syntax seems to be with where to explicitly specify the generic type if it cannot be inferred, I can understand the difficulty here. Would it be acceptable to omit the ability of explicitly specifying the generic type? If the type can't be inferred, could the type default to dynamic as in the example above? |
@polux wrote:
Thanks!
Dart type inference does not use unification; it's inference rather than (mere) type checking here, because the selection of an actual type argument for the invocation of @twistedinferno wrote:
Not quite, if no type arguments are provided, and there is no information which may be used for inference, the chosen type arguments are computed using an algorithm that we call instantiate-to-bound. This is a bit complex, but the basic idea is that we'll use the declared bounds as far as possible, and then approximate from above (that is, using a supertype) in the cases where the bound cannot be expressed precisely (e.g.,
That's a slippery slope, because developers would then be unable to solve the problem in the "natural" manner if they write invocations where inference cannot make a decision (but a developer might be able to help). So I suspect that it would be better to ask people to use a regular instance method (as opposed to an operator) if they need a generic function. That's possible today. |
@Aetet I think this could be a workaround class Foo<A, B> {
Foo<A, C> andThen<C>(Foo<B, C> that) => null;
Bar<A, B, C> call<C>() => Bar<A, B, C>(this);
}
class Bar<A, B, C> {
final Foo<A, B> source;
const Bar(this.source);
Foo<A, C> operator >>(Foo<B, C> lens) {
return source.andThen<C>(lens);
}
}
Foo<int, double> x;
Foo<int, String> y;
Foo<double, String> z;
Foo<int, String> res = x.andThen<String>(z);
Foo<int, String> res2 = x<String>() >> z; and a simpler example for anyone looking for how to get rid of method parentheses and also use generic types class FooGenericOperator<T> {
final Foo f;
const FooGenericOperator(this.f);
T operator >(T b) => f.operate(b);
}
class Foo {
R operate<R>(R b) => b;
FooGenericOperator<T> call<T>() => FooGenericOperator(this);
}
class Bar {}
Bar x = Foo().operate(Bar());
Bar y = Foo()<Bar>() > Bar(); |
@modulovalue Thanks for your response. It's interesting solution. |
@Aetet No I don't mind, I'd be honored. Thanks! |
I have a code like this:
instead of
I want to receive something like:
But operator overriding does not support generic method syntax so with implicit dynamic I got error from analyzer for
andThen
method.How I can achieve such functionality?
The text was updated successfully, but these errors were encountered: