Skip to content

"typedef" with reference name. #4309

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
srburton opened this issue Mar 29, 2025 · 4 comments
Open

"typedef" with reference name. #4309

srburton opened this issue Mar 29, 2025 · 4 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@srburton
Copy link

srburton commented Mar 29, 2025

Hello everyone, a good son returns home! 😊

I contributed to the bool.parse feature, and now I'm interested in contributing a new feature for typedef.

I’d like to gather community feedback on whether it makes sense for typedef, which serves as an alias for a function, to also carry a refname. This would allow scenarios like the following example to work:

Current Code

import 'dart:ui';

import 'package:flutter/widgets.dart';

typedef OnSetup = VoidCallback;
typedef OnClearName = VoidCallback;
typedef OnBtnClick = VoidCallback;

class People {

  static final Map<Type, List<Function>> _callbacks = {};

  /// Register a callback for the specific type
  static void on<A extends Function>(A callback) {
    _callbacks.putIfAbsent(A, () => []).add(callback);
  }

  /// Execute the callback registered of the specific type
  static void call<A>() {
    if (_callbacks.containsKey(A)) {
      for (var callback in _callbacks[A]!) {
        callback();
      }
    }
  }
}

main(){
    People .on<OnSetup>((){
       print('Hi');
    });

    People .on<OnClearName>((){
       print('Dart');
    });

    People .on<OnBtnClick >((){
        print('How are you?');
     });

    People.call<OnSetup>();
}

Output

Hi
Dart
How are you?

The expected

import 'dart:ui';

import 'package:flutter/widgets.dart';

typedef OnSetup = VoidCallback;
typedef OnClearName = VoidCallback;
typedef OnBtnClick = VoidCallback;

class People {

  static final Map<String, List<Function>> _callbacks = {};

  /// Register a callback for the specific type
  static void on<A>(A callback) {
    _callbacks.putIfAbsent(A.refname, () => []).add(callback);
  }

  /// Execute the callback registered of the specific type
  static void call<A.refname>() {
    if (_callbacks.containsKey(A.refname)) {
      for (var callback in _callbacks[A.refname]!) {
        callback();
      }
    }
  }
}

main(){
    People .on<OnSetup>((){
       print('Hi');
    });

    People .on<OnClearName>((){
       print('Dart');
    });

    People .on<OnBtnClick >((){
        print('How are you?');
     });

    People.call<OnSetup>();
}

Output

Hi

Justification

My proposal is to introduce a refname property to typedef, allowing comparisons based on the alias name rather than just the function type. This would make it easier to work with type-based mappings, such as event-driven architectures or dependency injection scenarios.

Impact

N/A

Mitigation

N/A

Change Timeline

N/A

Associated CLs

N/A

@lrhn lrhn changed the title [breaking change] "typedef" with reference name. "typedef" with reference name. Mar 31, 2025
@lrhn lrhn removed this from Breaking Changes Mar 31, 2025
@lrhn lrhn transferred this issue from dart-lang/sdk Mar 31, 2025
@lrhn
Copy link
Member

lrhn commented Mar 31, 2025

This is a languge request. You're proposing to have properties attached to types which are available on type variables.
See fx #4200 for what that might look like.

Further, you are proposing that function types that come from type aliases, and which are today completely indistinguishable (function types are structural types, they have no identity or declaration), will have something to distinguish the source (literally) of the type.
If you have:

typedef F1 = void Function();
typedef F2 = void Function();
void foo<T extends void Function()>() {
  // T from where ???
}
void main() {
  foo<F1>();
  foo<F2>();
  foo<void Function()>();
}

today, then there is no way to distinguish the types bound to T in foo from each other, because there is no difference, there is only one type for void Function() in the type system.

Which means that whatever is carried through the type parameter in your proposal is not the type, it's some kind of metadata which flows along with the type. Not impossible, but a little worrisome, because it means you need to correctly track the flow of types, even when the compiler might know that they're the same type.

What type is the refName? Is it a string? (Shouldn't be, then it risks not being unique.) Some opaque ID object?

Why only function types. You can type-alias other types too:

typedef Name = String;
typedef Address = String;
void main() {
  print(Name.refName == Address.refName); // false?
}

(This would presumably work, since the refName would be a getter on the Type object, so all Type objects have one.)

But not all types come from a type-alias. Not even all function types, so what is the refName of a type which doesn't come from a type alias?

At least it's a runtime property, so it's not affected by static type inference.

What would you expect this program to print?

typedef F1 = void Function();
typedef F2 = void Function();
class C1<T extends F1> {
  bool foo<X extends T>() => X.refName == F1.refName;
}
class C2<T extends F2> extends C1<T> {}
class C3<T extends F2> extends C2<T> {
  bool foo<X extends T>();
}
void main() {
  print(C1().foo());
  print(C1<F1>().foo());
  print(C1<F2>().foo());
  print(C2().foo());
  print(C2<F1>().foo());
  print(C2<F2>().foo());
  print(C3().foo());
  print(C3<F1>().foo());
  print(C3<F2>().foo());

  var c1 = C1();
  var c2 = C2();
  C1 c21 = c2;
  var c3 = C3();
  C2 c32 = c3;
  C1 c31 = c3;

  print(c1.foo());
  print(c2.foo());
  print(c21.foo());
  print(c3.foo());
  print(c32.foo());
  print(c31.foo());
}

I have my guesses.

@nate-thegrate
Copy link

nate-thegrate commented Apr 1, 2025

Maybe a good question to ask here would be "what's the workaround without using refname, does it look significantly worse?"


Extension types could work well here never mind, @mmcdon20 is right 😅

extension type OnSetup._(VoidCallback callback) {}
extension type OnClearName._(VoidCallback callback) {}
extension type OnBtnClick._(VoidCallback callback) {}

Maybe it's also worth noting that registering and calling a list of closures is exactly what ChangeNotifier was designed for.

All in all, when reading through the use case you've shared, my gut response is to be excited about several ways we could accomplish it via existing tools rather than wishing we had another language feature.

@mmcdon20
Copy link

mmcdon20 commented Apr 1, 2025

@nate-thegrate I don't think extension types works well here for two reasons.

  1. The extension types are equivalent to each other.
extension type OnSetup._(VoidCallback callback) {}
extension type OnClearName._(VoidCallback callback) {}
extension type OnBtnClick._(VoidCallback callback) {}

void main() {
  print(OnSetup == OnClearName); // true
  print(OnSetup == OnBtnClick); // true
  print(OnClearName == OnBtnClick); // true
}

As a result OnSetup, OnClearName, and OnBtnClick would all be considered the same key in the _callbacks map.

  1. Extension types on Function types cannot implement representation type (Allow extension type to implement Record and Function types #3839), which means you cannot pass them to a function that expects a VoidCallback or constrain the generic parameter to VoidCallback.

All in all, when reading through the use case you've shared, my gut response is to be excited about several ways we could accomplish it via existing tools rather than wishing we had another language feature.

I would agree, I think this can be solved in a number of ways with existing features, for example using an enum:

import 'dart:ui';

import 'package:flutter/widgets.dart';

enum Action { onSetup, onClearName, onBtnClick }

class People {
  static final Map<Action, List<VoidCallback>> _callbacks = {};

  /// Register a callback for the specific type
  static void on(Action action, VoidCallback callback) {
    _callbacks.putIfAbsent(action, () => []).add(callback);
  }

  /// Execute the callback registered of the specific type
  static void call(Action action) {
    if (_callbacks.containsKey(action)) {
      for (var callback in _callbacks[action]!) {
        callback();
      }
    }
  }
}

main() {
  People.on(Action.onSetup, () {
    print('Hi');
  });

  People.on(Action.onClearName, () {
    print('Dart');
  });

  People.on(Action.onBtnClick, () {
    print('How are you?');
  });

  People.call(Action.onSetup);
}

Output

Hi

@nate-thegrate
Copy link

The extension types are equivalent to each other.

void main() {
  print(OnSetup == OnClearName); // true
  print(OnSetup == OnBtnClick); // true
  print(OnClearName == OnBtnClick); // true
}

Oh wow, I had no idea! (I knew that the object returned by an extension type's "generative constructor" is identical to whatever it was wrapping, so I guess it makes sense for the types themselves to be identical as well.)


Extension types on Function types cannot implement representation type

Yeah, I remember that issue. I didn't bring it up here, mostly because I was confused about the A extends Function type argument—it seems like you'd always want to use a VoidCallback signature, since doing an empty function call for a signature with required parameters would crash at runtime… I didn't really want to get into the weeds about it :)


That enum example looks fantastic, thanks for putting it together!

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Apr 1, 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
Projects
None yet
Development

No branches or pull requests

6 participants