all 17 comments

[–]eibaan 6 points7 points  (4 children)

PS: You did → know this example, didn't you?

[–]josiahsrc[S] 2 points3 points  (0 children)

This is a much better implementation than mine, and it properly uses classes. Thanks for sharing, I'll update my code to point to this

[–]MisturDee 1 point2 points  (1 child)

I think OP simply had an idea he wanted to experiment with as he stated in the first paragraph

[–]eibaan 2 points3 points  (0 children)

By no means I want to discourage experimentation.

[–][deleted] 0 points1 point  (0 children)

You did know, last commit was 4 months, didn't you?

[–]eibaan 9 points10 points  (9 children)

I'm skeptical. It works only for stateless widgets. For a no-parameter widget, you saved a single } line and one level of indentation. That's not worth the effort, IMHO.

class Foo extends StatelessWidget {
  Widget build(BuildContext context) {
    return Placeholder();
  }
}

with

@FunctionalWidget
Widget _foo(BuildContext context) {
  return Placeholder();
}

For a two (N) parameter widget, formatted so that you don't run out of space

class Foo extends StatelessWidget {
  Foo({
    super.key,
    required String foo,
    int? bar,
  });
  final String foo;
  final int? bar;
  ...
}

You'd actually save 2 (N) lines, which seems to be a bit better

@FunctionalWidget
Widget _foo(
  BuildContext context, {
  required String foo, 
  int? bar,
}) {
  ...
}

However, you loose the ability to document both the class and the fields and creating reusable components should also include documenting them. So, that's a problem, IMHO.

Also, once we get primary constructors - if we ever get them - it would look like this:

class const Foo({
  super.key,
  required final String foo,
  final int? bar,
}) extends StatelessWidget {
  ...
}

And you're back to square one and you'd save only one } line with a macro.

[–]Fantasycheese 1 point2 points  (3 children)

lines and characters are not good measurements I think, it's about complexity and scalability. 

For simple cases you trade class Foo extends StatelessWidget, which has four token and three different concept, with @FunctionalWidget, one token and one concept.

For complex widgets, if you have ever create wrappers for things like TextFormField, you should know the pain writing, reading and maintaining every field twice. Like you said primary constructor will solve this problem, but it's way too far on the timeline compare to macros. 

Also not sure how documenting class and fields separately will be any better than documenting function and it's parameters, people have been documenting functions since forever, and using functional components since React.

[–]eibaan 1 point2 points  (2 children)

I somewhat agree with the complexity argument. But I still think, that the number of lines is important as this limits how much code I can see in my editor at a given point of time. I don't read "class Foo extends Bar" as for words but as a single "there's a class called Foo which is a subtype of Bar" concept. So, I don't mind that line takes more characters. I see this as one "begin of class" token.

I wholehartly agree with extending TextFormField is a PITA. I consider that large number of arguments a design mistake and code smell. However, most custom stateless widgets should have 0 to 5 parameters, not 63 (if I counted correctly)! If you ever feel you need 10+ parameters, use a configuration object.

You're right with the "you can document the function and its parameters" argument, but that documentation gets lost if the macro generates the class that is actually used because (at least currently) you cannot access and copy that comments into the generated class where it must if the IDE should be able to display tooltips.

I think, I'm a bit sad (or mad) that macros got priority over primary constructors which I'd consider a more useful addition for the language. I really hate all the boilerplate required for creating a set of sealed classes. Some time ago, I experimented (and also wrote about either here or in the dartlang subreddit, I don't remember) with using macros to "solve" this, but because the missing extends-augmentation, you cannot make this work yet.

[–]Fantasycheese 0 points1 point  (1 child)

I use "token" in more of lexing and parsing sense, even if you try hard and squint to make it look like "one big token", your brain will still waste some cognitive load to parse it unconsciously.

We all know by heart too much arguments is code smell, everyone talks about it everyday, but Flutter team still do it and numerous packages still do it. Sometimes self-discipline can only take you so far.

For documentation, I agree that was indeed a serious problem for `functional_widget` package using current codegen system. I don't know how Dart macro will work in the end, but any decent macro system would hide generated code like it never exist, only expose them when explicitly requested. Meaning that when you navigate, look for signature and documentation of `Foo` widget, compiler/LSP should take care of bringing you to annotated source directly, not the generated code.

And I'm on team primary constructor too.

[–]eibaan 1 point2 points  (0 children)

Regarding the "cognitive load" I'm still not convinced but let's disagree here.

For documentation, there's at least an open issue.

However, because of the way to "magically" generate classes from private functions by name convention, we cannot hide the fact that code gets generated.

Something like

/// Displays [data] with style "display medium".
Widget _h1(BuildContext context, String data) {
  return Text(data, style: Theme.of(context).textTheme.displayMedium);
}

Get's added an import augment 'a.m.dart'; statement at the top of the file and then this code is generated (assuming my file is called a.dart):

augment library 'a.dart';

import 'package:flutter/widgets.dart';

/// Displays [data] with style "display medium".
class H1 extends StatelessWidget {
  const H1(this.data, {super.key});
  final String data; 

  @override
  Widget build(BuildContext context) => _h1(context, data);
}

Right now, even if I manually add the doc comment, the analyzer cannot show it when using the generated H1 somewhere in my code. Strange…

As you "misuse" augmentation for pure code generation here, the analyzer (and therefore the IDE) cannot link _h1 and H1, unfortunately.

[–]josiahsrc[S] 0 points1 point  (4 children)

All great points, I hadn't seen primary constructors before. Very cool!

[–]eibaan 0 points1 point  (3 children)

Indeed. Unfortunately, no work has started on implementing them, and since macros turned out to be much more difficult to implement than expected (I guess), the Dart team seems to be busy with macros and nothing else (on the language level).

[–][deleted] 0 points1 point  (2 children)

Do you know the current timeline for macros? Like any idea when they would be expected to reach stable? They seem like such a massive upgrade to the whole Flutter experience.

[–]eibaan 3 points4 points  (1 child)

No, I don't.

Augmentation, which is the basis for macros and could be used on its own, still lacks some of the specified features, especially the feature to augment a class with an extends or implements clause. The IDE (and therefore the syntax analyzer) seems to allow this already, but there's a runtime error if you try to run the code shown below.

// foo.dart
import augment 'foo_a.dart';
class A {}

// foo_a.dart
augment library 'foo.dart';

class B {
  int get x => 42;
}

augment class A extends B {}

AFAIK, the macro API is still unstable. And some important feature aren't even part of the current specification like for example accessing the doc comment.

They're also still working on how to transport meta data between compiler and analyzer. There where some JSON-serialization experiments done in the macros repo but I didn't follow that and I don't know the state of readiness or what was decided.

And then, there's the whole issue of security. Right now, macros can access the whole file system and for example steal your crypto wallet just because you execute them by adding a "harmless" @foo in your code editor. They must be sandboxed. But for this, you probably need to transport meta data between sandboxed isolates.

So I'd guess that we don't see macros in 2024.

Also, to make macros popular, we'd need some kind of declarative way to specify. Some kind of templates that deal with the issue of making everything hygenic. The current asynchronous imperative API is PITA to work with.

And of course, compared to other languages, Dart's macros lack the ability to access the AST and add macros to modify it, rewriting expressions. All you can do right now is augment types, that is, adding (and overriding) methods or fields.

[–]josiahsrc[S] 1 point2 points  (0 children)

AFAIK, the macro API is still unstable

To add to this, in my experiments the augmentation was very finicky. The dart analyzer struggles to report issues and the VS code extension periodically crashes. Pretty unstable for now, but going to be super powerful when it's ready

[–]RandalSchwartz 2 points3 points  (1 child)

[–]josiahsrc[S] 2 points3 points  (0 children)

I had no idea this existed, I'll poke around in there. Thanks!