Skip to content

Support a type for "function that returns T" #26420

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

Closed
nex3 opened this issue May 6, 2016 · 11 comments
Closed

Support a type for "function that returns T" #26420

nex3 opened this issue May 6, 2016 · 11 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). type-enhancement A request for a change that isn't a bug

Comments

@nex3
Copy link
Member

nex3 commented May 6, 2016

Right now, there's no way to use Dart's type syntax to express a constraint on the return type of a function without also expressing constraints on its arguments. This makes it impossible to make certain classes of first-class functions strong-mode compatible.

The most notable function that needs this expectAsync() in test, which is widely used when testing asynchronous code. It takes a function (with up to six arguments and no named arguments) and returns a wrapper function with a compatible signature and some extra logic. In order to work with strong mode, it needs the output function to have a reified return type that matches the input function's, but there's no way to do that right now.

I propose that we add a generic parameter to Function representing its return type. That way we could write expectAsync() like so:

Function/*=F*/ expectAsync/*<T, F extends Function<T>>*/(Function/*=F*/ callback) {
  if (callback is _SixArgFunction) {
    return ([_0, _1, _2, _3, _4, _5]) => callback(_0, _1, _2, _3, _4, _5);
  } else if // ...
}

I'm marking this as S1 because it will be a barrier to the use of DDC with the test package once support for that exists (dart-lang/test#414).

@nex3 nex3 added area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). S1 high type-enhancement A request for a change that isn't a bug labels May 6, 2016
@lrhn
Copy link
Member

lrhn commented May 7, 2016

I know @eernstg has suggested Function<T> before. It has some advantages when you have code that uses Function.apply (which would then be typed as static T apply<T>(Function<T> function, List args, [Map<Symbol,dynamic> namedArgs])).

I'm still a little concerned that it's only half of a general solution. If you can take one of six function types, maybe you would be better off with a union type so you actually specify the allowed arguments too, and not just the return type.

About void functions: It would be great if "void" could be used as a type parameter as well. It would mean it could be used as a type in general. It might not make sense in a lot of places (at least not with non-nullable types), but it's more orthogonal if void is a first class type.

@eernstg
Copy link
Member

eernstg commented May 9, 2016

It is half a solution in the sense that it skips over the parameter list shape and the associated types, but the situation isn't symmetric:

(1) Knowledge about the return type is significant for lookups, and without that type (along with suitable runtime guarantees such that we can trust it) we have no knowledge about the nominal identity of the looked-up member. Without the return type for f, f().foo could look up any member whatsoever as long as its name is spelled foo; with the return type for f, f().foo will look up the foo declaration D which is the statically known one, or it will look up an overriding declaration D2 which is statically known to override D. This means that we can check D to see how to correctly use foo, including all the non-formal information we may get from dartdoc comments and the name of the enclosing class etc.etc. So this is significant for software correctness in a very concrete manner, and in a broader conceptual-as-well-as-technical sense.

(2) Knowledge about the parameter list shape and the parameter types is less tricky in relation to software correctness because it can be checked directly: If you get it wrong (and suitable runtime checks are active) then you'll get a clear message at the invocation. You are not going to go ahead and perform some completely unintended action that happens to have the same name.

That was my main motivation for recommending Function<T>: It allows programmers to take that important step toward typedness where the nominal identity of a subsequent lookup has been chosen, without going all the way to a fully specified function type.

@nex3
Copy link
Member Author

nex3 commented May 9, 2016

The return type is also significant for is and as checks in a way that's not symmetrical with parameter types. You can always create a function with optional dynamic argument types that passes an is check for any number of arguments less than some finite maximum, but the return type won't pass an is check unless it has the correct reified type. This is the root of the issue with expectAsync().

@eernstg
Copy link
Member

eernstg commented May 10, 2016

Very good, so we have lots of reasons for introducing Function<T>! Any showstoppers? ;-)

@scheglov
Copy link
Contributor

I must be missing something, but this code prints "true" and "false", as well as reports

ERROR: The argument type '() → String' cannot be assigned to the parameter type 'F() → int'. ([test] bin/my.dart:12)
ERROR: The argument type '() → String' cannot be assigned to the parameter type '() → int'. ([test] bin/my.dart:14)

Why do we need special support for return types?

typedef int F();

int foo() => 0;
String bar() => '';
void useF(F f) {}
void useF2(int f()) {}

main() {
  print(foo is F);
  print(bar is F);
  useF(foo);
  useF(bar);
  useF2(foo);
  useF2(bar);
}

@eernstg
Copy link
Member

eernstg commented Sep 30, 2016

Why do we need special support for return types?

You could argue that we cannot abstract over formal parameter list shapes, but it is at least possible to abstract over functions that accept k positional arguments (so the argument list declares at least k positional arguments, and it does not declare m > k required positional arguments) using definitions like

typedef T Func0<T>();
...
typedef T Func3<T>(Object x1, Object x2, Object x3);
...

up to some fixed maximal number N. I could have left out the argument types, but the plain Object makes it explicit that we do not need any special affordances, that is, we are not relying on exceptions in the type system for dynamic.

If you wish to wrap a given first class function such that the resulting wrapper function can be used in the context where the wrappee is expected then you must ensure that the wrapper has a reified type which will pass the (strong mode or Dart 2.0) checks intended for the wrappee. For any positional argument type in the wrappee we can use Object, because of the argument type contravariance. For the return type we have covariance, so the wrapper would have to have reified return type bottom (which is btw not expressible today, but we could just give it a name) in order to ensure that it will pass all dynamic checks that the wrappee would pass, but such a function wouldn't be able to return anything, so it still wouldn't be able to play the role of the wrappee.

If we could pass the return type along (by using Function<T> and possibly some inference to extract the T implicitly) then the wrapper function could be created to have that reified return type, and it would then be able to play the intended role.

(The actual code for expectAsync does more, of course, passing arguments around in a list with a _PLACEHOLDER value for handling optional arguments, but this is the core issue.)

So we need special support for return types because we cannot provide a type "that will always work" in a covariant position, as opposed to contravariant positions where Object will do fine.

@munificent munificent added this to the 1.50 milestone Oct 4, 2016
@leafpetersen
Copy link
Member

Following up here based on some discussion off of the issue tracker.

It looks to me like that there is not broad support for the kind of language changes that could address this in the short term, but rather a preference for one of several more general options that won't be available soon. I see this as leaving us as two options for the short term:

  1. Make expectAsync generic over the return type, and require the programmer to explicitly pass the generic argument:
  Function expectAsync<T>(Function f, ...) {... }

  Callback<int, int> f = expectAsync<int>((int x) => x) as Callback<int, int>;
  1. Split the expectAsync API into multiple functions based on arity, possibly with the unary case taking over the expectAsync name to take advantage of the fact that most uses are unary.
  F expectAsync0<T, F extends Callback0<T>>(T f()) => ...
  F expectAsync1<T, F extends Callback1<dynamic, T>(T f(dynamic x)) => ...
  ...

  Callback<int, int> f = expectAsync1((int x) => x);

Option 1 has the advantage that the API for non-strong mode stays the same, but it has the disadvantage that you get no static checking. If you don't pass a type argument, or if you pass the wrong one, you will get no static error, but you will get a runtime error in DDC.

Option 2 gives better static checking at the expense of a breaking change to the API.

@lrhn
Copy link
Member

lrhn commented Nov 1, 2016

On Tue, Nov 1, 2016 at 12:40 AM, Leaf Petersen [email protected]
wrote:

Following up here based on some discussion off of the issue tracker.

It looks to me like that there is not broad support for the kind of
language changes that could address this in the short term, but rather a
preference for one of several more general options that won't be available
soon.

I concur.

I see this as leaving us as two options for the short term:

  1. Make expectAsync generic over the return type, and require the
    programmer to explicitly pass the generic argument:

Function expectAsync(Function f, ...) {... }

Callback<int, int> f = expectAsync((int x) => x) as Callback<int, int>;

  1. Split the expectAsync API into multiple functions based on arity,
    possibly with the unary case taking over the expectAsync name to take
    advantage of the fact that most uses are unary.

F expectAsync0<T, F extends Callback0>(T f()) => ...
F expectAsync1<T, F extends Callback1<dynamic, T>(T f(dynamic x)) => ...
...

Callback<int, int> f = expectAsync1((int x) => x);

Option 1 has the advantage that the API for non-strong mode stays the
same, but it has the disadvantage that you get no static checking. If you
don't pass a type argument, or if you pass the wrong one, you will get no
static error, but you will get a runtime error in DDC.

Option 2 gives better static checking at the expense of a breaking change
to the API.

I can't find anything smarter than that.

Even by using configured imports to have a DDC specific version, I can't
see what to put in that version.

/L

Lasse R.H. Nielsen - [email protected]
'Faith without judgement merely degrades the spirit divine'
Google Denmark ApS - Frederiksborggade 20B, 1 sal - 1360 København K

  • Denmark - CVR nr. 28 86 69 84

On Tue, Nov 1, 2016 at 12:40 AM, Leaf Petersen [email protected]
wrote:

Following up here based on some discussion off of the issue tracker.

It looks to me like that there is not broad support for the kind of
language changes that could address this in the short term, but rather a
preference for one of several more general options that won't be available
soon. I see this as leaving us as two options for the short term:

  1. Make expectAsync generic over the return type, and require the
    programmer to explicitly pass the generic argument:

Function expectAsync(Function f, ...) {... }

Callback<int, int> f = expectAsync((int x) => x) as Callback<int, int>;

  1. Split the expectAsync API into multiple functions based on arity,
    possibly with the unary case taking over the expectAsync name to take
    advantage of the fact that most uses are unary.

F expectAsync0<T, F extends Callback0>(T f()) => ...
F expectAsync1<T, F extends Callback1<dynamic, T>(T f(dynamic x)) => ...
...

Callback<int, int> f = expectAsync1((int x) => x);

Option 1 has the advantage that the API for non-strong mode stays the
same, but it has the disadvantage that you get no static checking. If you
don't pass a type argument, or if you pass the wrong one, you will get no
static error, but you will get a runtime error in DDC.

Option 2 gives better static checking at the expense of a breaking change
to the API.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
#26420 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AEo9BySiKviBbIterGUOIOjDKQu_gIPEks5q5nxRgaJpZM4IZEDD
.

Lasse R.H. Nielsen - [email protected]
'Faith without judgement merely degrades the spirit divine'
Google Denmark ApS - Frederiksborggade 20B, 1 sal - 1360 København K

  • Denmark - CVR nr. 28 86 69 84

@leafpetersen leafpetersen removed their assignment Nov 5, 2016
@dgrove
Copy link
Contributor

dgrove commented Nov 9, 2016

@leafpetersen - any updates?

@leafpetersen
Copy link
Member

We do not plan to add Function<T>. The API will be split into N different calls by arity.

Details here: dart-lang/test#436

@floitschG is driving this.

@leafpetersen
Copy link
Member

Closing as not planned in favor of proposed changes to the expectAsync API.

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). type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

7 participants