-
Notifications
You must be signed in to change notification settings - Fork 205
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
Static Metaprogramming proposal. #1507
Comments
`;` // Don't forget the final semicolon! This is it. The concrete reason we've been waiting for. 😜 #69 |
The use case for whole new classes is so a developer can use the succinct function syntax to express some widget, but not suffer the performance penalty that comes with it. return [
_btn("btn1", onPressed: _handleBtn1),
_btn("btn2", onPressed: _handleBtn2),
]
// Flutter will perform better if this is turned into `_Btn1 extends StatelessWidget etc etc etc` but that is a bunch of wasted keystrokes for a developer
@extractWidget
Widget _btn1(String value. VoidCallback handler) => ... The requirement for this would be similar to the "Extract Flutter Widget" IDE shortcut, if the method does not have a reference to any class fields, it can be exctracred. Seems like there is another one as well that developers would really appreciate: @convertWidget
class MyWidget extends FlutterWidget {
MyWidget(this.foo, {Key? key}) : super(key: key);
final String foo;
@dispose
FocusNode _focusNode;
@override
void initState() {
super.initState();
_focusNode = FocusNode();
}
Widget build(BuildContext context) => TextFormField(focusNode: focusNode);
} Could be split into: class MyWidget_Generated extends StatefulWidget {
const MyWidget_Generated (this.foo, {Key? key}) : super(key: key);
final String foo;
@override
_MyWidget_GeneratedState createState() => _MyWidget_GeneratedState ();
}
class _MyWidget_GeneratedState extends State<MyWidget_Generated > {
late FocusNode _focusNode;
@override
void initState() {
super.initState();
_focusNode = FocusNode();
}
@override
Widget build(BuildContext context) => TextFormField(focusNode: _focusNode);
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
} |
I was thinking this as I typed it! @tatumizer, thanks for your feedback. Here are my responses:
See my second example, where I use
It's not magic, it's OOP. the macro-creator has to define an abstract class with the methods they want to generate, then the macro implements that class. It's the same as, instead of using macros, using
and
My proposal doesn't replace annotations -- |
@tatumizer I'll answer each of your points:
I was just giving a small example since I haven't actually used complex hashcodes (and it's worked for me). The point anyway was to demonstrate that you CAN, in fact, add fields. My hashCode example may be simple, but it still generates an int gettter. But let's make it just a little more complex: More complex
|
Agreed, which is why I didn't include it and instead opted for a
I also agree that the restrictions are arbitrary, but I disagree on their utility. Dart is the kind of langauge that doesn't let its users shoot themselves in the foot, and I think the guidelines are trying to enforce that (at least, the one "non-goal" in the doc). Anyhow, it makes my life easier 😁. This is just a first draft, and I'm thinking up of better designs in responses to your comments, so by no means is this the implementation. If we can think up of a way to modify existing code in an intuitive (and debuggable) way, then I guess I'll be all for it. |
Ya this seems problematic for a couple of the top use cases for flutter which is functional widgets, and a single-class widget that can hold state. In both cases it seems the original code should be replaced with something else, like the IDE does when running "Extract to Flutter Widget" or "Convert to Stateful Widget". For example, IDE turns
Into:
I often want to write (and more importantly, read) the former, but usually I have to write the latter because of implementation details in Flutter's Widget layer where it is optimized for classes over methods. |
You make a good point about the IDE's ability to delete and completely reshape code VS a macro. Users give permission to IDEs to do so (and can hit CTRL-Z when necessary) but macros seem to have an element of "magic" that makes me lean towards "don't touch my code!" What I think both of you are hitting at is the fine line between metaprogramming and making a function/class constructor. If you find yourself making Widgets similar enough to consider a macro, you can just make a Widget with a few parameters to handle the rest. In other words, instead of relying on method arguments, rely on constructor arguments. Like the example in the docs should be replaced with a |
You can certainly do this, and we all do do this today, but it becomes quite tedious and cumbersome in practice, when a typical app consists of hundreds of bespoke widgets and views, readability take a hit when everything needs to be it's own class. Just making a new widget is often a 10-15 line hit, and it can hurt readability because it moves the layout code away from the tree it sits inside of. Remi gives some good rationale here: https://pub.dev/packages/functional_widget, Basically in some scenarios the Widget solution makes sense, but in others, just having it be a method is easier to maintain. It would be nice if we didn't have our hands forced by performance considerations. One thing I'm sorta getting at is that the use cases for Flutter are rather limited, and I'm hoping we can take a pragmatic approach of just making something that knocks off those top 5 issues, vs something perfect and flexible, that takes many yrs to develop, and maybe in the end can still not do some of the basic stuff that we really need it for. With that said, I think everyone will be happy if we get just Data Classes and Serialization in a timely manner, and those seem rather straightforward 👍 |
I see how For example: @Stateless
Widget buildButton(String label, VoidCallback onPressed) =>
TextButton(child: Text(label), onPressed: onPressed); should generate class BuildButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
BuildButton({this.label, this.onPressed});
@override
Widget build(BuildContext context) =>
TextButton(onPressed: onPressed, child: Text(label));
} Not only is the name of the widget dependent on the name of the function, but its constructor list is as well. Other than that, the That being said, the language feature doc does say there will be a pre-processing step before the traditional "compile-time", so it's perfectly possible for it to be type-safe after all. I would just like to hear more about that from the Dart team before going there. |
That's not what metaprogramming is for. You don't want to generate a .dart file with the first n primes as literals, because now you have code with a list that's 2000 numbers long! You'd use a function instead. const int n = 2000;
List<int> getPrimes() => [
for (int num = 1; num < n; num++)
if (isPrime(num)) num
]; If you're manipulating data, you work with regular Dart, if you're manipulating code itself, you use metaprogramming. The whole point of metaprogramming is to add "code" as a first-class object, just like |
Of course I want! If I am not dealing directly with this list, so what? Generating it in runtime with a function will have the same cost in memory, but will be slower. Hardcoding the prime list with metastatic programming would make it faster and the syntax would be almost equal to using a function. Win-win. |
Just responding with my thoughts on the original proposal: In general this style of macro system has a lot of advantages - for instance knowing ahead of time the interface you will be implementing simplifies a huge number of things. It also means you can summarize a library (create an outline of its api) without ever even running the macro, which is a major advantage. At the same time it doesn't allow for adding apis you can't define ahead of time (copyWith). It also doesn't allow you to create new classes. Another feature I would like macros to support is implementing a field with a special getter/setter pair and private backing field, which I don't really see how this proposal could support. Ultimately we might end up compromising and doing something similar to this though, because it does have a lot of advantages. But we want to try and be more ambitious first. I do have a few more comments on some of the specifics of the proposal:
I don't think we want to use annotations to control the application of a macro to a class. I would prefer some new piece of syntax for applying macros. Something that makes it more obvious that code is actually being modified, annotations already are intended to only be metadata and not modify code directly. I think with your proposal especially it maps fairly directly to existing OOP concepts like mixins. One strawman could be to introduce a new keyword for applying macros, lets call that class ClassToAugment apply MacroName {}
I am not sure that we need the separate interface here or not... it is sort of nice in that it defines exactly what members are going to be added (and separates those from other methods/fields which are only used at compile time for instance). But it also feels like unnecessary boilerplate to me. |
I edited the proposal to include feedback from @tatumizer (see the I also bit the bullet on generating signatures and included @mateusfccp @tatumizer @jakemac53 I have to go, I'll respond to you in about an hour. |
@tatumizer this proposal is pretty specifically tied to classes, so I don't see the need for what you are describing. We would only need to add the |
I was working on this as you commented! I added it to the original proposal, let me know what you think.
Do you mean caching a field? I wrote an example in a previous comment, but I'll recreate it here:
|
So more specifically a common use case is observable fields. Lets say I have a class like this: class Person {
String email;
} And I want to do something any time Fwiw I had a proposal a while back which was very close to what you have here but I think it just doesn't translate well to this type of use case, and I think its a very powerful/important use case to solve for. |
Not necessarily. In fact, I specifically used class MyMacro extends Macro { } having class JsonMacro extends ClassMacro { }
class FunctionTimer extends FunctionMacro { }
class MakeParameterDynamic extends ParameterMacro { } // useless, but I can't think of anything better
class ExportMyUtils extends LibraryMacro { }
// etc.
I don't understand. I also assumed the class JsonMacro extends Macro {
final bool includeNullValues;
const JsonMacro(this.includeNullValues);
}
@JsonMacro(false)
class User { } |
Fwiw as far as "parameters" for macros I think those are probably best provided as annotations. The macro itself should not be an annotation but it can read annotations for configuration purposes. Unless we do macro functions which look like normal function calls, then regular parameters might be ok (but they should probably be forced const). |
Still works, thanks to Validatorimport "dart:mirrors";
class Validate {const Validate();}
const validate = Validate(); // to be used as annotation
class ValidateFields extends Macro {
const ValidateFields([Mirror mirror]) : super(mirror);
@generateWithSignature
generate() {
for (final MethodMirror field in getFieldsWithAnnotation(Validate)) {
// Ignore the "_" in front
final String name = getSymbolName(field.simpleName).substring(1);
final Type type = field.returnType.reflectedType;
`#type get #name => _#name;`
`set #name(#type value) {`
` _#name = value;`
` print("User changed #name to $value")`
`}`
}
}
} Example@ValidateFields()
class User {
@validate
String _email;
@validate
String _password; // what do you mean, encryption?
// Does not need to be validated
int id;
}
// Generated code:
class GeneratedUser {
@validate
String _email;
@validate
String _password;
int id;
String get email => _email;
set email(String value) {
_email = value;
print("User changed email to $value");
}
String get password => password;
set password(String value) {
password = value;
print("User changed password to $value");
}
} |
Ah ok, I see how you are using I am also generally not much of a fan of using annotations to control the behavior here (annotating the methods to be included in the class feels... weird). |
@jakemac53 Yeah, @tatumizer I think we both agree then that we need some sort of annotation-like syntax to apply macros -- whether that be |
Re: function macros, what would a typical use-case look like? I'm trying to make a function macro: class FunctionMacro<T extends Function> extends Macro<T> {
const FunctionMacro(FunctionTypeMirror mirror) : super(mirror);
} But it's hard to know exactly what it will look like without knowing the uses for it. The most common one is a function timer/logger, but that can already be achieved by just wrapping it in another function. Future<Duration> timer(Future<void> Function() func) async {
final Stopwatch stopwatch = Stopwatch();
print("Starting function");
stopwatch.start();
await func();
stopwatch.end();
print("Function took ${stopwatch.elapsedMilliseconds}ms to run.");
return stopwatch.elapsed;
}
const Duration delay = Duration(seconds: 1);
Future<void> delayedFunction(String name) {
await Future.delayed(delay);
print("Hello, $name!");
await Future.delay(delay);
}
void main() {
final Duration duration = timer(() => delayedFunction("Alice"));
} Anyway, |
Function macro is just really a word I made up. More specifically what I meant is defining macros as essentially normal functions that take a single parameter which is the thing that they will modify (and they also potentially would be able to add sibling declarations). These would potentially be invoked with some special syntax (I will use So one strawman example re-using some concepts like the backtick strings from your proposal there could be an equality macro that looks like this: /// I think you want some special keyword like `macro` to define these, they would always have a
/// return type of `void` and would only be available at compile time.
macro equality(ClassMirror clazz) {
// Add new declarations with specialized methods on the "mirror" types
clazz.addMethod(
`bool operator == (Object other) {`
`return other is #{getSymbolName(mirror.reflectedType)}`
for (final MethodMirror field in clazz.instanceMembers.values)
if (field.isGetter) {
var name = getSymbolName(field.simpleName);
`&& this.#name == other.#name`
}
}
`;` // Don't forget the final semicolon!
);
} You would then potentially use this like this, with a special equality! class User extends EasyHashCode {
String name;
int age;
} The mirror types would also have The type of the parameter to the macro determines which types of declarations it is allowed to be invoked on, so you can get some type safety there. It is also easily extendable to any and all types of declarations. Obviously I don't have a full proposal in place for this yet though :). |
I like this. Similarly to what I suggested originally, basically a new code block syntax #1482 (comment). but with the parameters. Now on usage, what about another code block? apply equality {
class User extends EasyHashCode {
String name;
int age;
}
} (nesting is at the core of Dart, and maybe we should try to keep it this way) |
I think the block just isn't adding anything here other than extra indentation that isn't really necessary. If you want to apply several macros then you start getting very nested etc. |
Another nice property of the "macro functions" (may as well double down on the name now lol) is they are very composable. We could allow invoking them exactly like normal functions from within other macros, as well as manually chaining them at the application site. So you could do: equality! hashCode! class User {...} Or you could define a macro that combines them for convenience: macro dataClass(ClassMirror clazz) {
equality(clazz);
hashCode(clazz);
}
dataClass! class User {...} I think that is pretty cool :) |
@jakemac53 Do you see any use case of data sharing between macros? |
Not really no, I would expect separate macros to be completely independent of each other. That property is a big part of what makes them composable. If you need to do a whole bunch of things to a class and share some higher level state to do it I would expect you to just use a single larger macro to do that. |
We need to be careful with virus. If one downloads a code and open IDE and things start running in the background. |
Sorry if I was confusing, I was referring to a macro that modifies a function, as opposed to adding methods to a class. Hence my Also, I do like the class JsonMacro extends Macro { } with macro JsonMacro { } It better highlights that there is some magic going on in terms of giving the macro access to a
But then how would you pass in parameters to a macro? How would you write the following? class DebugInfo extends Macro {
final String classPurpose;
const DebugInfo(Mirror mirror, this.classPurpose) : super(mirror);
@generate
void debug() {
`print("The class #{getSymbolName(mirror.simpleName)} is used for #classPurpose");
}
}
@DebugInfo("To use both Mac and Windows terminals")
class TerminalUtils { }
@DebugInfo("To do operations on Strings")
class StringUtils { } As a side note: Would we rather replace the |
@idkq Agreed -- macros should only generate code as part of compilation, not automatically. |
Why would a regular import not work? |
It doesn’t solve the problem. I can open a code that writes a binary file when compiled. It might not be able to execute it, but it can definitely write it. |
Well, to be fair, you can do that now -- and you can even execute it! import "dart:io";
void main() {
File("virus.bat")
.writeAsStringSync("rmdir C:\Windows\System32"); // lol get rekt
Process.runSync("virus.bat");
} |
Yeah, I recognize that. If mirrors can be salvaged, then great. Otherwise, we can use another API. The feature doc actually suggested using the Analyzer API, which sounds about right to me. Speaking of the Analyzer, it works just fine with ensuring type safety of imports. I understand that we're adding a new pass of the compiler, but if your IDE can read and analyze imports without running any code, then so can a code generator, no? |
Stumbled on another nice use case today. In this lib: https://github.com/Skycoder42/firebase_database_rest They want us to implement a
or a more realworld example:
|
With the The next question is: Why stop at two levels? Can you create code which creates code? (If not, why not?) The way Java annotation processing works (AFAIK) is that the compiler parses a partial program, with some files missing. Then it detects an annotation recognized by a registered annotation processor, and invokes that annotation processor. The only thing that processor can do is to generate one or more files. After that, those new files are found, parsed and incorporated into the partial program. Then it repeats until all registered annotations have been processed and there are no missing files in the imports. That means that a code generator can generate code with annotations triggering new code generation (so your fancy code generator can generate code depending on, say, built_values, and not need to specify that it must run prior to the built_values generator). I think we should support something like that iterative model, and not, in any way, depend on a specific processing order. Whether we generate code inside the existing library or as external files ( The Java model has simplicity going for it. It requires the compiler/annotation-processor to be able to handle a partial program, where some identifiers are still unbound and some members are missing, and the ability to check whether a currently missing import has become available, but that's basically it. The actual code generation is completely up to the processor (and not all annotation processors need to generate code, some could be doing extra static checks instead). You can use whichever framework you want to generate the actual Java code files, including writing strings directly to a file. The unit of generation is a file. The file can exist after compilation, which means error messages can point directly to it. If code doesn't exist in a tangible form, then error messages in generated code becomes harder to handle. |
@lrhn Interesting, appreciate the rundown. Is Dart ready to have a compilation process like Java, or is it a major change? And how, if needed, can we modify this proposal to work best with that? I was hoping we could have a way to generate code that wasn't as prone to syntax-errors as strings, but if the compiler can generate the code and immediately analyze it, I'm sure it won't be much of an issue. |
I've been saying it since day 1 but somehow we keep going back to annotations. Should be clear by now that annotation is not code, should not be code, should not regulate how code is generated or compiled or packed. |
I've also been advocating this whole time for using |
I think your example shows we're talking about two seperate ideas. I'm looking to generate code, not data. Your example doesn't generate code, it creates compile-time constants. The "how" in this case is that Zig evaluates comptime expressions and exposes their results in |
I agree that there is a difference between generating raw code for compilation and code interpreted by compiler. I believe the goal is to generate raw, inspectable code. Not something magic or coupled. In the example above there is no way to inspect the code, unless I'm missing something. BTW, const data is code. |
Not in this sense. When you write a const variable and compile, your .dart file doesn't get magically updated to contain the value of that const variable -- it's just evaluated during compilation instead of at runtime. Like having |
Well, it actually does. The constant evaluator is a kernel transform which quite literally replaces the constant expression with a constant value in the kernel code. The backends (for the most part) only see the evaluated value, not the expression. |
Right, but that's the compiled code. I meant the actual user-readable Dart code (that's why I said "not in this sense"). I'm assuming that this proposal would lead to a .dart file of generated code that devs can inspect and debug for themselves. In other words, I'm more interested in generating methods and classes than something like this, even if it is possible: final List<int> nPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]; // ... |
Closing this in favor of my new proposal, #1565. Feel free to reply with your thoughts there, although let's try keeping the comments there relevant so it doesn't blow up. |
If functions could be const, I assume you could do the following to achieve the same result as compTime?
|
New, updated proposal in #1565
A proposal for a design for static metaprogramming (taken from my discussions in #1482).
Asking @jakemac53 @lrhn @leafpetersen for their feedback since they were involved in the above issue.
The language feature request gives a lot of detail, and discusses what properties a good implementation for metaprogramming (called macros) would have. Here are some points worth repeating:
Approach:
dart:mirrors
or the Analyzer API.The doc also points out common use cases:
==
andhashCode
StatefulWidget
s (especially the.dispose()
method) andStatelessWidget
I propose a design for writing and invoking macros, and address the pros and cons. I put code in drop-downs since I include a lot of examples and comments and want to keep the document neat.
The general spec
Here's how macros would work
Common use cases:
JSON serialization
Generated code:
toString()
,==
, andhashCode
Generated code:
Flutter's
State.dispose()
Generated code:
EDIT:
copyWith
Generated code:
These examples are predictable, and, with the
dart:mirrors
docs handy, really easy to write. Thebacktick
and#
-syntax aren't too confusing, and allow for clean interpolation, just like "quotes" and$
s, respectively. Now let's look at what it does and does not address:Approach
@generate
and@generateWithSignature
annotationsMacro
class.dart:mirrors
or the Analyzer API.dart:mirrors
, but anything similar can be used.@generateWithSignature
output a generated signature instead of the one defined in the macro.Checklist
dart:mirrors
, or some other system, a specialMacro
class is able to expose details about the code to the macro being written.@generateWithSignature
, you can specify the signature in backticks as well.ColumnWidget
with parameters as needed. I'm still unsure how metaprogramming would practically create new classes without a lot of manual help.StatelessWidget
s, without modifying the existing code (to save readability). I'll work on it.DataClassMacro
invokeToJson
). But this can probably be solved by havingDataClassMacro
extendToJson
instead of justMacro
.#
, expressions can be injected into the generated code.@generate
or@generateWithSignature
annotations.I'd love to hear feedback and possible improvements.
Changes
EDIT 1: I made two revisions based on feedback:
@generate
or@generateWithSignature
annotations. The examples have been updated@generateWithSignature
annotation, you can now define a macro whose signature is not known ahead of time.EDIT 2: I added a
copyWith
example to the examples above.The text was updated successfully, but these errors were encountered: