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

Support generic arguments to operators #30048

Closed
Aetet opened this issue Jun 29, 2017 · 28 comments
Closed

Support generic arguments to operators #30048

Aetet opened this issue Jun 29, 2017 · 28 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). closed-not-planned Closed as we don't intend to take action on the reported issue type-enhancement A request for a change that isn't a bug

Comments

@Aetet
Copy link

Aetet commented Jun 29, 2017

I have a code like this:

class GetterLens<A, B> {
...
    GetterLens<A, C> andThen<C>(GetterLens<B, C> that) => that.compose(this);
    GetterLens operator >> (GetterLens lens) {
      return this.andThen(lens);
    }
}

instead of

  var plusElevenLens = plusOneStrLens.andThen(plusTenLens);

I want to receive something like:

  var plusElevenLens = plusOneStrLens >> plusTenLens;

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?

@floitschG floitschG added the area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). label Jun 29, 2017
@floitschG
Copy link
Contributor

floitschG commented Jun 29, 2017

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 o.[](i) or o.>>(x)), then there would be a place to put the generic arguments. There are no concrete plans to add those, though.

@floitschG floitschG changed the title How to use operator overload with generic methods? Support generic arguments to operators Jun 29, 2017
@Aetet
Copy link
Author

Aetet commented Jun 29, 2017

Any workaround for this?

@floitschG
Copy link
Contributor

Any workaround for this?

not really. You either need to use a function, or make the types less strong.

@Aetet
Copy link
Author

Aetet commented Jun 29, 2017

with less strong types - won't be auto inference. With function it'll be as hard to read as direct function invocation. 😞

@floitschG
Copy link
Contributor

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.

@Aetet
Copy link
Author

Aetet commented Jun 29, 2017

Ok. I see. Thanks.

@lrhn lrhn added the type-enhancement A request for a change that isn't a bug label Jun 29, 2017
@lrhn lrhn added the closed-not-planned Closed as we don't intend to take action on the reported issue label Sep 1, 2018
@atreeon
Copy link

atreeon commented Dec 5, 2018

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)

@lrhn lrhn closed this as completed Dec 6, 2018
@lrhn
Copy link
Member

lrhn commented Dec 6, 2018

There is no reasonable way to add generics to operators, because there is no way to provide the type argument when using the operator.

@atreeon
Copy link

atreeon commented Dec 6, 2018

Could it work if the type was inferred? If you can't infer the type then just set as dynamic.

@polux
Copy link
Contributor

polux commented Dec 6, 2018

@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.

@atreeon
Copy link

atreeon commented Dec 6, 2018

@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).

var fn = <T1>(T1 x) => x;
fn(5) is int; //true

@polux
Copy link
Contributor

polux commented Dec 6, 2018

@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.

@atreeon
Copy link

atreeon commented Dec 6, 2018

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.

  var fn = (x) => x;
  var result = fn(5);
  result is dynamic; //true

@atreeon
Copy link

atreeon commented Dec 6, 2018

ah, sorry, I think I understand what you mean. That example isn't right, it is not generic!

@atreeon
Copy link

atreeon commented Dec 6, 2018

  var fn = <TInt>(TInt x) => x;
  var result = fn(5);
  print(result is dynamic); // true

@atreeon
Copy link

atreeon commented Dec 6, 2018

print(result is int); // true

@polux
Copy link
Contributor

polux commented Dec 6, 2018

What I mean is something like this:

f<A>(A x) {
   return (A == int) ? 1 : 2;
}

Let's say f was an operator: then when calling f(foo), depending on the ability of the compiler to infer the type of foo the expression would evaluate to either 1 or 2.

@atreeon
Copy link

atreeon commented Dec 6, 2018

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 :-)

var list = new List();
 list.add(5);
 list.add("5");

 for (var i = 0; i < 10; i++) {
   var result = list[Random().nextInt(1)];
   // print("list:" + result == int ? "1" : "2");
 }

@eernstg
Copy link
Member

eernstg commented Dec 6, 2018

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

.. the inference algorithm would have to be part of the spec ..

Already simple stuff like Iterable<int> xs = []; depends on type inference, both during static analysis and at run time, so it should certainly be specified. We'll get there.

@twistedinferno wrote

I think it would already work in a lot of situations [For example:]

var fn = <T1>(T1 x) => x;
fn(5) is int; //true

What happens here is that you are testing that the result returned by fn is an int at run time, and that's not relying on any inferred types (that would succeed also if you gave fn the actual type argument Object or anything else that still allows for the invocation):

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 int when you call fn(5), and you can see that in a different way:

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 fn via its runtimeType getter, and this shows that it is a generic function. If you use a function declaration (like fnNamed) such that you are accessing the function directly (rather than passing around a function object and obtaining it by evaluation of an expression like fn), then you can also get a generic instantiation of a generic function, when it's assigned to a target whose type is a non-generic function. That's how you get a function of type int Function(int) called fnInt. This is similar to currying, in that fnInt is derived from fnNamed by passing an inferred type argument (with the value int), and the resulting object is a non-generic function, i.e., it is still ready to accept its value parameters.

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 result has a type which is a subtype of the type dynamic. That's not hard to achieve, because every object has that property (because every type is a subtype of dynamic).

@polux wrote

What I mean is something like this:

f<A>(A x) {
   return (A == int) ? 1 : 2;
}

Let's say f was an operator: then when calling f(foo), depending
on the ability of the compiler to infer the type of foo the expression
would evaluate to either 1 or 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.

@polux
Copy link
Contributor

polux commented Dec 6, 2018

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.

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.

@eernstg
Copy link
Member

eernstg commented Dec 6, 2018

I assumed that in case the type inference of the type argument of a
function failed, the program would be rejected

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.

@polux
Copy link
Contributor

polux commented Dec 6, 2018

Ok. To me that was type checking because it doesn't require unification but I guess that's still type inference indeed.

@polux
Copy link
Contributor

polux commented Dec 6, 2018

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".

@atreeon
Copy link

atreeon commented Dec 7, 2018

many thanks for explaining so well @eernstg (and for reminding me that is is not the same as .runtimeType and also for ignoring that List() explicitly returns List<dynamic>()!) I think the following example shows that if a generic type is not specified then it defaults to dynamic.

class MyGen<T1> {}
print(MyGen().runtimeType); //MyGen<dynamic>

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?

@eernstg
Copy link
Member

eernstg commented Dec 7, 2018

@polux wrote:

.. thanks ..

Thanks!

it doesn't require unification

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 f may rely on information from subterms (like actual arguments) as well as information from the context.

@twistedinferno wrote:

if a generic type is not specified then it defaults to dynamic.

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., X extends C<X>).

Would it be acceptable to omit the ability of explicitly specifying the generic type?

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.

@modulovalue
Copy link
Contributor

@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();

@Aetet
Copy link
Author

Aetet commented May 9, 2019

@modulovalue Thanks for your response. It's interesting solution.
Every year I make presentation about little dart puzzlers for local conference.
Old Presentation
I use your solution at new presentation if you won't mind and mention you as author?

@modulovalue
Copy link
Contributor

@Aetet No I don't mind, I'd be honored. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). closed-not-planned Closed as we don't intend to take action on the reported issue type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

7 participants