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

Calling Generic Type Anonymous Functions #3380

Closed
LeoBound opened this issue Oct 3, 2023 · 4 comments
Closed

Calling Generic Type Anonymous Functions #3380

LeoBound opened this issue Oct 3, 2023 · 4 comments
Labels
request Requests to resolve a particular developer problem

Comments

@LeoBound
Copy link

LeoBound commented Oct 3, 2023

Is it possible to do something like the following?

class FunctionContainer<T, R> {
  final R Function(T) function;
  final T argument;

  const FunctionContainer(this.function, this.argument);
}

void main() {
  final Set<FunctionContainer> functions = {
    FunctionContainer<String, int>(int.parse, "3"),
    FunctionContainer<String, double>(double.parse, "3.141"),
    FunctionContainer<String, String>((s) => s, "Hello"),
  };

  final results =
      functions.map((container) => container.function.call(container.argument));
  print(results);
}

I would expect the result to be a List<dynamic> containing [3, 4.141, "Hello"], and statically the compiler doesn't detect any issues with the code.
Instead I get TypeError: Closure 'int_parse': type '(String, {((String) => int)? onError, int? radix}) => int' is not a subtype of type '(dynamic) => dynamic' at runtime.

Although the example above doesn't seem hugely motivated, I would like to use a similar pattern to create JSON (Map<String, dynamic>) from some form fields.

Is there a workaround for this?

Thanks

@LeoBound LeoBound added the request Requests to resolve a particular developer problem label Oct 3, 2023
@eernstg
Copy link
Member

eernstg commented Oct 3, 2023

This is a well-known issue. First, there is a workaround:

  final results =
      functions.map((container) => (container as dynamic).function(container.argument));

You used container.function, and this is an expression which is very likely to give rise to a run-time error. For example, it is guaranteed to cause a run-time error in the case where the static type of container is FunctionContainer (which is the same thing as FunctionContainer<dynamic, dynamic>) and the run-time type of container is FunctionContainer<String, int> or any of those other types that we actually have in this example.

The underlying issue is that you are using a "non-covariant member", that is, a member like function whose declared type has a non-covariant occurrence of a type parameter of the enclosing class FunctionContainer. In short, the problem is that you have an instance variable of type R Function(T) (it's no problem that R is used as a return type, but it is a problem that T is used as a parameter type in the type of this variable).

With this in mind, we can see that the dynamic type of container.function can be String Function(int), and the static type is dynamic Function(dynamic). The former is not a subtype of the latter, so it would be a soundness violation to allow container.function to evaluate to that function. In other words, we're not allowed to read that variable when the receiver has that static type, so we get a run-time error instead.

When we use (container as dynamic).function the evaluation succeeds because we've said explicitly that we don't want to consider container to have type FunctionContainer<dynamic, dynamic>. So we can read the variable, obtain the function object, and call it.

You can do several things to handle this issue at a more fundamental level. One thing you could do is to change the class FunctionContainer such that the invocation of the function occurs inside the class (which mean that you have access to the type parameter T, such that the invocation can be type safe):

class FunctionContainer<T, R> {
  final R Function(T) _function;
  final T argument;

  const FunctionContainer(this._function, this.argument);
  R invokeFunction() => _function(argument);
}

void main() {
  final Set<FunctionContainer> functions = {
    FunctionContainer<String, int>(int.parse, "3"),
    FunctionContainer<String, double>(double.parse, "3.141"),
    FunctionContainer<String, String>((s) => s, "Hello"),
  };

  final results =
      functions.map((container) => container.invokeFunction());
  print(results);
}

Another technique you could use would be to change FunctionContainer to be invariant in T, as described here.

However, this wouldn't fit your use case so well because you want to store functions with different parameter types in the same set, and that's inherently unsafe. If you change FunctionContainer to be invariant in T then the type checker will insist that the parameter type of the actual function must be a supertype of the value of T, and that basically means that T must be Never, and then you still need to perform the invocation of the functions in a way which is not type safe.

@eernstg
Copy link
Member

eernstg commented Oct 3, 2023

I'll close this issue because it's all described in other issues as mentioned.

@eernstg
Copy link
Member

eernstg commented May 15, 2024

@LeoBound, I'd like to add another comment on this issue, because of the narrow scope of the proposed solution.

It is indeed true that replacing container.function(container.argument) by container.invokeFunction() eliminates the run-time type error. However, it wouldn't work if we need to choose which argument to pass to the given function, rather than relying entirely on one fixed choice.

So let's generalize slightly and consider the situation where we have a list of arguments, and we'd like to pick and choose the arguments from that list and pass them as actual arguments to the function.

We can do this be "opening" each FunctionContainer, using the getter typerOfR to get access to the value of the type argument R. (We could easily add a typerOfT to get access to T as well, but R will suffice for now).

import 'package:typer/typer.dart';

class FunctionContainer<T, R> {
  final R Function(T) _function;
  final List<T> arguments;

  const FunctionContainer(this._function, this.arguments);
  R function(T argument) => _function(argument);

  Typer<R> get typerOfR => Typer();
}

String listTypes(List<Object?> list) {
  var buffer = StringBuffer('[');
  buffer.write(list.map((r) => '${r.runtimeType}').join(', '));
  buffer.write(']');
  return buffer.toString();
}

void main() {
  final Set<FunctionContainer<String, Object>> functions = {
    FunctionContainer<String, int>(int.parse, ["3", "4"]),
    FunctionContainer<String, double>(double.parse, ["3.141", "2.7181"]),
    FunctionContainer<String, String>((s) => s, ["Hello", "world!"]),
  };

  final results =
      functions.map((container) {
          var typer = container.typerOfR;
          return typer.callWith<List<Object?>>(<R>() {
            // Inform the type system about guaranteed typings.
            var containerR = container as FunctionContainer<String, R>;
            var result = <R>[];
            for (var argument in containerR.arguments) {
              result.add(containerR.function(argument));
            }
            return result;
          });
      }).toList();

  print('Values: $results');
  print('Types: ${listTypes(results)}');
}

// NB: The explicit type argument to `callWith` is only needed because of
// https://github.com/dart-lang/sdk/issues/55644.

In the invocation of callWith, the actual value of the type parameter R of container is passed as the type variable R of the function literal <R>() {...}, and this means that we can use that type variable directly. The type system doesn't understand it, but we do have a guarantee that container has type FunctionContainer<String, R>, and we use that to get a reference to container with a better type, namely containerR. At this point it is statically safe to pass argument to containerR.function.

This shows that you need more machinery in order to be able to work safely with a class like FunctionContainer, but it is possible.

@LeoBound
Copy link
Author

@eernstg Thanks for the update. I've actually been trying to solve a similar scenario recently in a different project and this has solved it 🙂 I wasn't aware of the typer package but it looks helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

2 participants