Skip to content

IIFE type inference #2820

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

Open
modulovalue opened this issue Feb 5, 2023 · 9 comments
Open

IIFE type inference #2820

modulovalue opened this issue Feb 5, 2023 · 9 comments
Labels
feature Proposed language feature that solves one or more problems type-inference Type inference, issues or improvements

Comments

@modulovalue
Copy link

modulovalue commented Feb 5, 2023

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: #2820 (comment)

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 modulovalue added the feature Proposed language feature that solves one or more problems label Feb 5, 2023
@lrhn lrhn added the inference label Feb 5, 2023
@modulovalue
Copy link
Author

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
Copy link
Author

@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
Copy link

I think I may have encountered the same issue when I was experimenting with pattern matching. dart-lang/sdk#51395 (comment)

@eernstg
Copy link
Member

eernstg commented Feb 14, 2023

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
Copy link
Member

lrhn commented Feb 14, 2023

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
Copy link
Member

eernstg commented Feb 15, 2023

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
Copy link
Author

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.

@modulovalue modulovalue changed the title IIFE type inference. IIFE type inference and data flow analysis issues. Feb 15, 2023
@eernstg
Copy link
Member

eernstg commented Feb 15, 2023

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
Copy link
Author

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.

@modulovalue modulovalue changed the title IIFE type inference and data flow analysis issues. IIFE type inference Feb 15, 2023
@lrhn lrhn added type-inference Type inference, issues or improvements and removed inference labels Apr 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems type-inference Type inference, issues or improvements
Projects
None yet
Development

No branches or pull requests

4 participants