-
Notifications
You must be signed in to change notification settings - Fork 224
Description
There have been a few discussions related to this (such as jakemac53/macro_prototype#29), but I don't think there's a problem language issue for it yet, so here it is
Long story short, the idea for this proposal is to allow metaprogramming to define macros that are applied on expressions, such users can write:
void function() {
final value = @macro(params);
}
And the macros would be able to replace the final value = @macro(params);
code (but not what is before and after this code) with a modified expression.
This could enable a variety of useful patterns, such as:
Implementing an await-like keyword for Widget builders
A common issue with Flutter widgets is that they tend to get rapidly nested:
Widget build(context) {
return StreamBuilder<A>(
stream: aStream,
builder: (context, AsyncSnasphot<A> aSnapshot) {
return ValueListenableBuilder<B>(
animation: bListenable,
builder: (context, B b, _) => Text('${a.data} $b),
);
}
);
}
This is an issue similar to Future.then
, which the language solves with await
, but Widgets have no equivalent.
With expression macros, we could instead write:
Widget build(context) {
AsyncSnapshot<A> aSnapshot = @StreamMacro(aStream);
B b = @ValueListenableMacro(bListenable);
return Text('${a.data} $b);
}
and the macros would desugar this code into the previous code
Stateful functional widgets
Through expression macros, it could be possible to drastically simplify stateful widgets by offering a @state
macro, typically combined with a @functionalWidget
macro, such that we'd define:
@functionalWidget
Widget $MyWidget(_MyWidgetState state) {
var count = @state(0);
return Column(
children: [
Text('Clicked $count times'),
ElevatedButton(onTap: () => count++),
]
);
}
And this would desugar var count = @state(0)
into:
int get count => state.count;
set count(int value) => state.count++;
such that full final code would be:
@functionalWidget
Widget $MyWidget(_MyWidgetState state) {
int get count => state.count;
set count(int value) => state.count++;
return Column(
children: [
Text('Clicked $count times'),
ElevatedButton(onTap: () => count++),
]
);
}
class MyWidget extends StatefulWidget {
const MyWidget({ Key? key }) : super(key: key);
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int _count = 0;
int get count => _count;
set count(int value) {
setState(() => _count = value);
}
@override
Widget build(BuildContext context) {
return $MyWidget(this);
}
}
(this assumes that we can define local gettters/setters, but I think that's a reasonable request)
Guards
A common thing when dealing with null
or functional classes like Result
/Either
is to do:
Class? function() {
final a = getA();
if (a == null) return null;
final b = getB();
if (b == null) return null;
<some logic>
}
or:
Result<Class> function() {
Result<A> a = getA();
if (a.isError) return Result<Class>.errorFrom(a);
Result<B> b = getB();
if (b.isError) return Result<Class>.errorFrom(b);
<some logic>
}
...
This could be simplified by a @guard
macro, such that we would define:
Class? function() {
final a = @guard(getA());
final b = @guard(getB());
<some logic>
}
and this would generate the if (x == null) return null
/ if (x.isError) return Result<T>.errorFrom(x)
for us.
Custom compiler optimisations
Rather than simplifying existing code, an alternate use-case would be to make existing code more performant.
A use-case I personally have is with Riverpod, where users can do:
const provider = Provider<A>(..);
const anotherProvider = Provider<B>(...)
<...>
class Example extends ConsumerWidget {
const Example({ Key? key }) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
A a = ref.watch(provider);
B b = ref.watch(another);
...
}
}
where ref.watch
works by doing a Map.putIfAbsent
, such that the previous ref.watch
usages are roughly equal to:
@override
Widget build(BuildContext context, WidgetRef ref) {
A a = ref.listeners.putIfAbsent(provider, () => provider.listen(...));
B b = ref.listeners.putIfAbsent(another, () => provider.listen(...));
...
}
This works fine, but it would be better if we could remove this Map
. With expression macros, we could possibly implement a @watch
, such that we'd have:
@override
Widget build(BuildContext context, ExampleRef ref) {
A a = @watch(provider);
B b = @watch(another);
...
}
and this would equal:
@override
Widget build(BuildContext context, ExampleRef ref) {
A a = ref.providerListener ??= provider.listen(...);
B b = ref.anotherListener ??= another.listen(...);
...
}
abstract class ExampleRef {
Subscription? providerListener;
Subscription ? anotherListener;
}
effectively removing the Map
in the process by exploding it into class properties
Dependency tracking without AST parsing
Using the same code as the previous example, Riverpod would like to generate a static list of dependencies associated with a "provider".
The idea is that with:
final a = Provider<A>(...);
final b = Provider<B>(...);
final example = Provider<Example>((ref) {
A a = ref.watch(a);
String str = ref.watch(b.select((B b) => b.str));
return <...>
}, dependencies: {a, b});
we would like macros to generate the dependencies
parameter of example
. The problem being, existing macro proposals currently do not allow macros to access the AST. Which means something like:
@autoDependencies
final example = Provider<Example>((ref) {
...
});
would not be able to search for ref.watch
invocations to generate that dependencies
list.
But if we replaced ref.watch
with a @watch
macro, we could possibly have that @watch
do the generation, such that individual @watch
usage would all declare their parameter in the dependencies
list
Note:
I am aware that this use-case would likely require subtle changes to the Provider
usage to work. Because macros likely wouldn't be able to push items into a collection. The important is being able to statically extra calls to ref.watch
within the function. From there, the Provider
definition could change to match the requirements.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status