dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

IIFE type inference #2820

Open modulovalue opened 1 year ago

modulovalue commented 1 year ago

Consider the following program that does not compile because type inference fails to infer Foo<int> as the type of the immediately invoked function expression (IIFE):

void main() {
  <Foo<int>>[
    Foo(),
    // Error: A value of type 'Foo<dynamic>' can't be assigned to a variable of type 'Foo<int>'.
    //   - 'Foo' is from 'package:dartpad_sample/main.dart' ('lib/main.dart').
    //  }(),
    //  ^
    // Error: Compilation failed.
    (){
      return Foo();
    }(),
  ];
}

class Foo<T> {
  Foo();
}

I find IIFEs to be very useful because they allow me to not pollute scopes with names that don't need to be there. I regularly experience type inference issues related to IIFEs. I think it would be great if Dart could have better support for IIFE type inference.

Edit: See also: https://github.com/dart-lang/language/issues/2820#issuecomment-1431253037

Edit: I think that this issue applies to all function literals (i.e. () {}, () sync* {}, () async {} and () async* {} because it doesn't look like the type checker needs to depend on their evaluation strategy.

modulovalue commented 1 year ago

Here is another instance of this issue that is closer to a useful real world example:


void main() {
  R match_example<R>(
    final int t,
    final R Function() zero,
    final R Function() one,
    final R Function() other,
  ) {
    if (t == 0) {
      return zero();
    } else if (t == 1) {
      return one();
    } else {
      return other();
    }
  }

  List<String> a() {
    const value = 7;
    return [
      "a",
      "b",
      "c",
      ...match_example(
        value,
        () => ["d"],
        () => ["e"],
        () => [],
      ),
    ];
  }

  List<String> a_with_iife() {
    return [
      "a",
      "b",
      "c",
      // The element type 'dynamic' can't be assigned to the list type 'String'.
      ...() {
        const value = 7;
        return [
          ...match_example(
            value,
            () => ["d"],
            () => ["e"],
            () => [],
          ),
        ];
      }(),
    ];
  }

  a();
  a_with_iife();
}

The type of the IIFE in a_with_iife can't be inferred.

Note (related to the support of IIFEs as a language feature): Unfortunately, an analyzer provided refactoring Convert to expression body involving an IIFE with a single return statement produces invalid Dart programs. I've also noticed that the overhead of the function call in IIFEs does not necessarily get optimized away during compilation.

modulovalue commented 1 year ago

@munificent It looks to me like eventually you would like to support the following once records have landed:

Foo(
 ...(
   a: ...,
   b: ...,
 ),
)

I couldn't find the issue for that feature. I think the inference issue presented here is also related to that feature because being able to do:

Foo(
 ...() {
   // Other statements.
   return (
     a: ...,
     b: ...,
   );
 }(),
)

without any type inference issues seems like it would be very useful. I hope you will consider supporting ergonomic IIFEs in your design.

mmcdon20 commented 1 year ago

I think I may have encountered the same issue when I was experimenting with pattern matching. https://github.com/dart-lang/sdk/issues/51395#issuecomment-1429010479

eernstg commented 1 year ago

One thing you could do is to abstract away the immediate invocation:

// Use this one to "immediately invoke" your function literals.
X iife<X>(X Function() f) => f();

// This one is just needed in order to see the context type.
X whatever<X>() {
  print(X);
  return <Never>[] as X;
}

void main() {
  // With an actual immediate invocation the returned expression is inferred with context type `dynamic`.
  List<int?> xs = (){ return whatever(); }(); // Prints 'dynamic'.

  // When using `iife`, the context type is carried over.
  List<int?> ys = iife(() => whatever()); // Prints 'List<int?>'.
  List<int?> zs = iife(() { return whatever(); }); // Ditto.
}

The typing doesn't depend on the choice of () => e or () { return e; }.

lrhn commented 1 year ago

We don't have the ability to have a context type of "Function which returns X, but I don't know the argument shape". That's the context we'd like to use for f in T v = f(args);, which we want to infer types for before we infer for the arguments.

eernstg commented 1 year ago

I don't think we need to abstract over the shape of the parameter list. The 'IIFE' acronym refers to a function literal which is immediately invoked, that is, (){...}() (nice ASCII art, btw ;-), and it's quite unlikely that we'd want to declare and pass any parameters to an IIFE. The iife function that I mentioned actually solves the problem at hand. It would be used as follows in the examples:

X iife<X>(X Function() f) => f();

class Foo<T> {
  Foo();
}

void main() {
  <Foo<int>>[Foo(), iife(() => Foo())]; // No errors.
}

The longer example is similar (omitting a because that function caused no problems):

X iife<X>(X Function() f) => f();

void main() {
  R match_example<R>(
    final int t,
    final R Function() zero,
    final R Function() one,
    final R Function() other,
  ) {
    if (t == 0) {
      return zero();
    } else if (t == 1) {
      return one();
    } else {
      return other();
    }
  }

  List<String> a_with_iife() {
    return [
      "a",
      "b",
      "c",
      ...iife(() { // No errors.
        const value = 7;
        return [
          ...match_example(
            value,
            () => ["d"],
            () => ["e"],
            () => [],
          ),
        ];
      }),
    ];
  }

  a_with_iife();
}

The syntactic cost is obvious, but small (except that (){ return Foo(); }() is actually longer than iife(() => Foo()) ;-). There is no run-time cost, because iife is statically resolved and will surely be inlined.

modulovalue commented 1 year ago

The iife function that I mentioned actually solves the problem at hand.

I agree that the iife function can be used to simulate IIFEs and that it solves the type inference issue that I was referring to, but it can't be used to solve one issue that I forgot to mention.

Consider the following:

void main() {
  inlined();
  literal();
  function();
}

int inlined() {
  int? b;
  b = 0;
  return b;
}

int literal() {
  int? b;
  () {
    b = 0;
  }();
  return b;
}

int function() {
  int? b;
  iife(() {
    b = 0;
  });
  return b;
}

R iife<R>(
  final R Function() fn,
) {
  return fn();
}

The function-expression-based IIFE, and the iife-function-based IIFE fail to compile because the data flow analysis that allows inlined to compile is not able to see across function boundaries.

This data flow analysis related issue (in addition to the type inference one) prevents refactoring tools from adding support for wrapping expressions/statements in (){ ... }() while guaranteeing that the semantics of the program will not be changed.

eernstg commented 1 year ago

Right, the error about b being potentially null exists because the flow analysis makes no attempt to track the usages of a function literal (even a function literal whose only usage is right there at the function literal itself, because it's an IIFE), and this means that b is classified as non-promotable. The flow analysis basically says "this function body could run at any time", with some improvements if it is possible to see statically that some parts of the function body will always execute before this particular function literal has been evaluated.

But I'd say that this is a completely different topic, so maybe it belongs in an issue which is specifically about issues with, or improvements of, flow analysis and promotion?

modulovalue commented 1 year ago

But I'd say that this is a completely different topic

I agree. I initially did not intend for this issue to include usability improvements related to () { ... } () beyond type inference. By sharing the last example I wanted to highlight that your suggestion (i.e. to use an iife function), while solving the problem that this issue initially raised, in my opinion, is not a satisfactory solution.

so maybe it belongs in an issue which is specifically about issues with, or improvements of, flow analysis and promotion?

Yes, I agree. I will open a separate issue for that example.