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

Type inference compilation error on callable class #3122

Closed
nank1ro opened this issue Jun 1, 2023 · 4 comments
Closed

Type inference compilation error on callable class #3122

nank1ro opened this issue Jun 1, 2023 · 4 comments
Labels
bug There is a mistake in the language specification or in an active document

Comments

@nank1ro
Copy link

nank1ro commented Jun 1, 2023

Code giving error visible at https://dartpad.dev/dfd112dd6e0be277c2d8f3c3f7c6577f

I put the code also here for an easy reference:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final a = A("0");
    return MaterialApp(
      theme: ThemeData.dark(),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: MyWidget(
          fn: a,
           builder: (context, value, _) {
             return Text(value);
           }
          ),
        ),
      ),
    );
  }
}

class A<T> {
  A(T value): _value = value;
  
  final T _value;
  
  T call() => _value;
}

class MyWidget<T> extends StatelessWidget {
  const MyWidget({
    super.key,
    required this.fn,
    required this.builder,
  });
  
  final T Function() fn;
  final ValueWidgetBuilder<T> builder;

  @override
  Widget build(BuildContext context) {
    return builder(context, fn(), null);
  }
}

This code throws a compilation error like this:

Error: The argument type 'Object?' can't be assigned to the parameter type 'String'.
 - 'Object' is from 'dart:core'.
             return Text(value);

The interesting thing is that just by changing fn: a into fn: a.call the error disappears and the code works as expected.
Also this fn: () => a() works.
Seems like the Type inference doesn't work correctly on callable functions when the function name is not explicitly set.

At the time of creating this issue these are the Dart and Flutter versions being used:

  • Flutter 3.10.2
  • Dart SDK 3.0.2
@nank1ro nank1ro added the bug There is a mistake in the language specification or in an active document label Jun 1, 2023
@eernstg
Copy link
Member

eernstg commented Jun 1, 2023

There is no information flow that will inform the choice of formal parameter types of a function literal based on code in the body of the function literal.

In other words, the type of value in (context, value, _) { return Text(value); } will never take into account that this parameter is passed to an invocation of a Text constructor. The type of function literal parameters (without a declared type, like all three parameters in this case) is obtained from the context type, if any.

In this case the context type is ValueWidgetBuilder<T>, which is the declared type of the instance variable builder and hence also the type of the parameter this.builder in the constructor.

The value of T in this case is Object? (the greatest closure of the empty context _), because no elements in the code contribute any constraints about T.

Hence, passing value as in Text(value) is a compile-time error.

I think the main thing to emphasize here is that there is no way to transfer information from the body of a function literal to the types of formal parameters, they get everything (if anything) from the context type of the function as a whole. So in the case where the formal parameter types aren't inferred as you would expect, please double check that there is some useful information in the context type.

I'll close the issue because it is all working as intended.

@eernstg eernstg closed this as completed Jun 1, 2023
@nank1ro
Copy link
Author

nank1ro commented Jun 1, 2023

Sorry @eernstg but based in the informations you given I still don't understand why with this little change:

- fn: a,
+ fn: a.call,

everything works as expected without any compilation error.

Isn't a supposed to work exactly like a.call given that a has a callable function inside it?

@eernstg
Copy link
Member

eernstg commented Jun 1, 2023

Sorry, I missed that question.

The 'callable class' feature is a residue after a more general mechanism in Dart 1: An instance of a class that declares a method named call was, at the time, a full-fledged function, and its run-time type was a real function type (and a 'function type' is something like void Function(int), but not Function). This means that an instance of a 'callable class' would work like a first class function object, period.

However, this feature doesn't come for free. In particular, if every function invocation is really an implicit invocation of the call method of an object then it needs to perform a normal instance method dispatch. This is more expensive than jumping directly to the address of the code based on a pointer which is stored in the function object itself (plus some calling convention things like allocating an activation record etc, but that's needed for the instance method invocation as well).

So we removed this very general mechanism and replaced it by a very shallow compile-time mechanism: If an instance of a statically known class that defines a method named call occurs in a context where a function is expected, that instance is modified to tear off its call method.

So if a occurs with a context type which is Function or a function type then it is transformed into a.call.

This won't happen if you use the expression (a as dynamic), it is only applied to expressions whose static type is a class that implements a call method.

However, a is subject to static analysis before we know that it might need to become a.call (because we can't answer that question before we know whether the static type of a indeed does define a call method), and this means that the static type of a.call isn't going to help type inference of the enclosing object, that type inference step will just see the static type of a (which is not a function type) and then probably give up on making the type of a match the context type. Later, static analysis will see that the static type of a doesn't match the context type, but it helps to make it a.call; so we do that, but type inference is already done, so the newly discovered function type doesn't do its job.

That's probably the reason why MyWidget(...) is inferred as MyWidget<Object?>(...), but when you change a to a.call then the function type serves as a source of some extra constraints on T, and then we might get MyWidget<String>(...) (if that's what it takes to make the overall inference succeed).

Are you sure you actually need to use a callable class in this situation? Couldn't it simply be a function? I think it's a good habit to rely as little as possible on this particular mechanism (that is, a --> a.call), also because it's going to go away as soon as we can get away with it. ;-)

Cf. #2399.

@eernstg
Copy link
Member

eernstg commented Jun 1, 2023

By the way, you might want to use a more strict static analysis:

warning • n027.dart:69:34 • The type of value can't be inferred; a type must
          be explicitly provided. Try specifying the type of the parameter.
          • inference_failure_on_untyped_parameter

based on the following 'analysis_options.yaml':

analyzer:
  language:
    strict-raw-types: true
    strict-inference: true
    strict-casts: true

linter:
  rules:
    - avoid_dynamic_calls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug There is a mistake in the language specification or in an active document
Projects
None yet
Development

No branches or pull requests

2 participants