language
language copied to clipboard
Proposal for a pipe-like operator to chain constructor/method invocations without nesting
Problem:
One common complaint with Flutter is how widget trees tend to get very nested. There are various attempts at solving this, such as using the builder pattern. Cf: https://www.reddit.com/r/FlutterDev/comments/hmwpgm/dart_extensions_to_flatten_flutters_deep_nested/
More recently, this was suggested during the Flutter in production livestream
But relying on chained methods to solve the problem has numerous issues:
- We loose the ability to use
constconstructors - We reverse the reading order.
a(b(c()))becomesc().b().a() - It involves a lot of extra work to allow people to create their own widgets.
Using builders, we need to define both a class, and an extension method that maps to said class:
class MyWidget extends StatelessWidget { const MyWidget({this.child}); } // Necessary extension for every custom widget, including any necessary parameter extension on Widget { MyWidget myWidget() => MyWidget(child: this); } - A widget is sometimes used as
Class()and sometimes as.class(), which feels a bit inconsistent - There's no way to support named constructors. For example,
ListViewhasListView()andListView.builder(). But as methods, at best we'd have.listView()vs.listViewBuilder() - It breaks "go-to-definition", "rename" and "find all reference" for widgets. Using "go to definition" on
.myWidget()redirects to the extension, and renaming.myWidget()won't renameMyWidget()
Proposal:
I suggest introducing two things:
- a new "pipe-like" operator, for chaining constructor/method class
- a keyword placed on parameters to tell the "pipe" operator how the pipe should perform. Such keyword could be used on any parameter. Positional or named, and required or optional. But the keyword can be specified at most once per constructor/function, as only a single parameter can be piped.
The idea is that one parameter per constructor/function definition can be marked with pipable. For example:
class Center extends StatelessWidget {
Center({
super.key,
required pipable this.child, // We mark "child" as pipable
});
final Widget child;
}
When a parameter is marked as such, the associated constructor/function becomes usable in combination with a new "pipe" operator. We could therefore write:
Center()
|> DecoratedBox(color: Colors.red)
|> Text('Hello world');
Internally, this would be strictly identical to:
Center(
child: DecoratedBox(
color: Colors.red,
child: Text('Hello world'),
)
)
In fact, we could support the const keyword:
const Center()
|> DecoratedBox(color: Colors.red)
|> Text('Hello world');
Note:
Since in our Center example, the child parameter is required, it would be a compilation error to not use the pipe operator.
As such, this is invalid:
Center();
Similarly, the |>operator isn't mandatory to use our Center widget. We are free to use it the "normal" way and write Center(chid: ...).
In a sense, the combo of the pipable keyword + |> operator enables a different way to pass a single parameter, for the sake of simple chaining.
Conclusion:
This should solve all of the problems mentioned at the top of the page.
- We can still use
conston a complex widget tree - The reading order is presserved (top-down instead of bottom-up)
- Supporting this syntax doesn't involve creating a new extension everytimes
- Widgets are always used through their constructor
- Named cosntructors are supported
- IDE-specific operation such as "renaming/go-to-definition/..." keep working.
As a bonus, the syntax is fully backward compatible. It is not a breaking change to allow a class to be used this way, as the class can still be used as is this feature didn't exist.
On first glance I like this proposal because conceptually it seems similar (for me) to the way that this is passed in as a hidden (or even somtimes not hidden) parameter to methods in OOP languages and that makes it easy for me to quickly get my head around the concept 👍🏻
See also https://github.com/dart-lang/language/issues/1246 (Feb 17, 2014)
I like the idea, but I think we should consider using a different operator. Since I’m using a Turkish layout keyboard, pressing four keys to type the |> operator is a bit inconvenient. It might also be the case for other keyboard layouts. Perhaps we could use something like this instead:
const Center()
- DecoratedBox(color: Colors.red)
- Text('Hello world');
@kevmoo fwiw, although the concept is similar, widgets would be a tough to use with just #1246
The "pipable" keyword discussed here is key to simplifying widget usage
Andf the result wouldn't be const either when possible
Pipe is a bit pain to type. Colon?
const Center()
:> DecoratedBox(color: Colors.red)
:> Text('Hello world');
Yes, I also think that it is very important to maintain the order of widgets. And we also need some simplifications. And I like the proposed approach. It will allow me to choose how I want to write the code without losing the overall logic and structure of the widgets. What I mean:
For example:
Example 1:
- I can use the flattest writing style when necessary:
Container(width: 200, height: 200, alignment: .center, color: .green)
|> DefaultTextStyle(style: TextStyle(color: .black))
|> Text('Bloc', textAlign: .center, textScaler: TextScaler.linear(2));
- Or vertical, if I need to describe more parameters, for example:
Container(
width: 200,
height: 200,
color: .green)
|> DefaultTextStyle(
style: TextStyle(color: Colors.black),
)
|> Text(
'Hello World',
textAlign: TextAlign.center,
);
Some additional thoughts:
t may also be interesting to extend the logic of working with the |> operator. For example, we can introduce an additional context for the with operator, which will be used to define more complex logic for building parameters, callbacks, or builders. This will increase the readability and separation of the logic from the UI:
Example 2:
- For the logic of parameters:
Container(width: 200, height: 200, color: Colors.grey)
|> with (
color: isActive ? Colors.green : Colors.red, // logic for parameter color
alignment: Alignment.center,
child: (child) => Padding(
padding: EdgeInsets.all(isActive ? 16 : 8),
child: child,
),
)
|> Text(label, style: TextStyle(color: Colors.white));
- To derive the logic of the handlers:
Center(
|> ElevatedButton(style: ButtonStyle(padding: EdgeInsets.all(5.0)),)
|> with (
onPressed: () {}, // separated callback
child: (child) => Text("Tap Me!"),
);
This would allow us to have a separate description of UI components from handlers, which looks quite convenient and clean. It looks like a kind of paired extension class or functional mutator.
As I understand it, this will work too?
Example 3:
Column(mainAxisAlignment: MainAxisAlignment.center)
|> [
Text('Item 1'),
Text('Item 2'),
Text('Item 3'),
]
|> Padding(padding: EdgeInsets.all(8));
It would also be interesting to have branching in a pipable container, for example, something like this:
Example 4:
Container(width: 200, height: 200)
|> (isActive ? Text('Active') : Text('Inactive'));
Although, in theory, it could be part of the general “with” extension:
Container(width: 200, height: 200)
|> with (child: (_) => isActive ? Text('Active') : Text('Inactive'));
These are just a few ideas for discussing how to simplify and improve UI and code composition. And my impressions of the proposed approach
I REALLY like this proposal!!! (Sorry for the !!! but I'm really excited)
- It is way more concise while preserving the tree structure
- It will remove the
child/childrenparameter from the tree which I was previously hoping to get via positional parameters, but this is even better as it also removes a lot of noise in the the formatting
If we could combine that with automatic currying of the last parameter of function calls that get used with the piping operator we wouldn't even need a new pipable keyword but only to move the pipable parameter to the last position of the constructor.
On the question of keyboard layouts I want to reply, that for German keyboards |> isn't ideal to type either BUT if this would be added the first thing I would do is a keyboard-shortcut in VS code. Making the pipe operator similar as in other languages and clearly distinctive while reading is more important IMHO. Plust with the progress of AI you probably won't need to type that yourself anymore
I really like this as it
- preserves the top-down structure, which is easier to read than proposed decorators bottom-up
- adding a widget in a deep tree structure is just 1 line git diff
- It's basically Nested with language support
Example with a bit more nested structure:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Padding(
padding: EdgeInsets.all(5),
child: ColoredBox(
color: Colors.red,
child: SizedBox(
width: 50,
child: Text('Item 1'),
)
),
)
),
Text('Item 2'),
Text('Item 3'),
],
)
vs
Column(mainAxisAlignment: MainAxisAlignment.center)
|> [
Center()
|> Padding(padding: EdgeInsets.all(5))
|> ColoredBox(color: Colors.red)
|> SizedBox(width: 50),
|> Text('Item 1'),
Text('Item 2'),
Text('Item 3'),
]
and just an idea, while it is not really important now, about syntax, maybe formater might not even put space in front of |> since its already big enough and IMO still readable?
Column(mainAxisAlignment: MainAxisAlignment.center)
|> [
Center()
|> Padding(padding: EdgeInsets.all(5))
|> ColoredBox(color: Colors.red)
|> SizedBox(width: 50),
|> Text('Item 1'),
Text('Item 2'),
Text('Item 3'),
]
@tenhobi The side effect on how much easier it would get to review UI PRs with Diffs is a bit plus
I find this proposal very compelling. It looks amazing from a Flutter perspective. I'm just wondering if any other language has such a syntax.
I agree the language should support beautiful Flutter code, but this solution doesn't look good imo. I'd claim state management must have special language support too (like Svelte had).
@tenhobi Would it work? Reading the proposal:
a keyword placed on constructor parameters Center(required pipable this.child,)
I read var children = [Text('Hi')] as:
var children = new List<Text>();
children.add(new Text('Hi'));
I guess it could work, but the proposal would need to be expanded to have an "operator |>" and add it in the List class.
@Wdestroier That would make all widgets not be const and child not to be final.
@mraleph If we just threw out const would this make Flutter apps slower? Would it also guarantee that future improvements to the dart compiler for const widgets never help Widget code?
The Flutter Maintainers would just need to add pipable and add a dart fix and call it a day. However this looks like a massive language feature. @rrousselGit is cooking today!
@Wdestroier I don't understand what wouldn't work? It would work just as Flutter works today, you can just put a parameter marked as pipeable outside after the constructor/function call. Basically just a syntactic sugar, right?
If you put it in print (just a stupid example), then:
void print(pipeable String text);
print()
|> 'Hello world';
If you put it in Column, then
class Column ... {
const Column({required pipeable List<Widget> children});
...
}
const Column()
|> [
Text('a'),
Text('b'),
]
Column()
|> [
const Text('a'),
Text('b = $value'),
]
This has the opposite direction of #1246.
That pipe would require you to write
Text(…)|>DecoratedBox(…)|>Center()
Which has the same inversion of order issue as chained methods.
This sounds like a way to place one argument outside of the argument list, presumably in order to indent it less.
It feels similar to cascades to me, just at the argument list level. It's a way to keep doing something without indenting for each instance.
Center()::child:
DecoratedBox(color: .red):: child:
Text(theText)
or
Center()
:::child: DecoratedBox(color: .red)
:: child: Text(theText)
Maybe what is needed is just a different formatting, not a language feature? Probably hard to do something consistent. Still, something like
Center(child:
DecoratedBox(
color: .red, child:
Text(theText)
))
which the formatter divines from some hint put on the child parameter.
@tenhobi Can you explain the green arrow?
I took the original example:
Center()
|> DecoratedBox(color: Colors.red)
|> Text('Hello world');
and changed the arrow direction:
Center()
<| DecoratedBox(color: Colors.red)
<| Text('Hello world');
It looks more understandable, I guess.
using <| would also fix the correctly pointed out problem by @lrhn that piping would work in the wrong direction
@Wdestroier i corrected the code, it was a mistake (the last |> Padding...)
Sorry if I step in, but imho using that syntax inside the Widget tree is very unreadable and confusionary. I'd love to have the pipe operator (|>) in order to write functions inside the business logic, but certainly not for creating Widgets.
@lrhn
Center()::child: DecoratedBox(color: .red):: child: Text(theText)or
Center() :::child: DecoratedBox(color: .red) :: child: Text(theText)
Making ALL dart code have the ability to pass arguments in 2 different ways:
print() |>"Hello"print("Hello")
would divide the Flutter/Dart community, which is one of the major drawbacks of the entire decorators proposals. (besides for the entire argument about how it's actually harder to understand blah blah blah)
I don't want to see a Tabs vs Spaces fight for no good reason.
Adding a pipable argument is a significant ask & (as far as I know) there is nothing like it in any other language, but unless there are technical reasons why it doesn't solve the problem I think this an awesome way to go.
- Backswords Compatible
- No Crazy Indentation
- No trailing
);}]);at the end of complex widget trees.
However from a language perspective I imagine this would require massive changes to the analyzer and all the code generation libraries.
Making ALL dart code have the ability to pass arguments in 2 different ways:
* `print() |>"Hello"` * `print("Hello")`
@dickermoshe That's not how the pipe operator works.
print("Hello") becomes "Hello" |> print() and not the opposite, because you pipe the value you have before |> as the first argument of the next function
Some other ideas for the operator, e.g. -> would be quite easy to type in most keyboard layouts. I also think we should maintains at least basic nesting to represent the hierarchy.
Column(mainAxisAlignment: MainAxisAlignment.center, spacing: 8)
-> [
Center()
-> Padding(padding: EdgeInsets.all(5))
-> ColoredBox(color: Colors.red)
-> SizedBox(width: 50),
-> Text('Item 1'),
Text('Item 2'),
Text('Item 3'),
]
with ligatures:
or
Center()
{ DecoratedBox(decoration: ...)
{ Padding(...)
{ Text('Hello world'); }}}
I think we should indeed use the <| to correctly show what it passed into which else and the indentation created the tree structure.
@orestesgaolin Let's not get caught up on semantics yet, we don't know if dads letting us get a dog so don't start coming up with names for him yet
Yeah the name of that pipable keyword or the |> used in the example don't really matter at this stage.
Maybe what is needed is just a different formatting, not a language feature? Probably hard to do something consistent. Still, something like
Center(child: DecoratedBox( color: .red, child: Text(theText) ))which the formatter divines from some hint put on the
childparameter.
A core problem is also how () are simplified.
Flutter tends to have code that ends in )}})))}) 🤷
Changing the formatting wouldn't help that. There's already a new formating with similar ideas in mind coming to Dart 3.7, and it's not enough
I do like the idea of viewing this as a cascade-like operator though. But a cascade-like operator would raise the topic of named VS positional parameters
And the formatting is likely still doing to be very indented for that.
I assume we'd have:
Center()
:: child = DecotedBox(color: .red)
:: child = MyButton();
If we format everything on the same indent level, that'd raise the question of "which class are we initializing"?
Unless that cascade operator would only allow a single initialization? But then that doesn't really feel cascade-like.
So something more specialized makes sense IMO. It's important to remove those indents
First two notes about the syntax:
- Right associativity of
|>makes it confusing. All other operators in Dart are left associative.a * b * care(a * b) * c. Buta |> b |> cis actuallya |> (b |> c). - Direction of the arrow is confusing - the data actually flows right to left, but the arrow points in the opposite direction.
I don't think everything needs to be a special syntax / language feature. Constructors are great because they are just a language feature, which exists in one form or another in most languages. Decorators (as explored by @loic-sharma and the team) are just extensions methods, which fundamentally also means they are just calls. Easy to understand again. Center() |> Text() or Center() <| Text()- well, that is extremely dense and specific. It is unreadable if you don't know what you are reading. Compare this to Text().centered() or Center(child: Text(...)) - both of which are much more readable (without knowing much about Dart/Flutter even).
FWIW we could (almost) experiment with this style without any special language features:
class PipeWidget<P extends Widget, C extends Widget> extends StatelessWidget {
final P parent;
final C child;
const PipeWidget({required this.parent, required this.child});
@override
Widget build(BuildContext context) {
return wrap(parent, child);
}
}
extension WrapInWidget1<P extends Widget> on P {
PipeWidget<P, Widget> operator <<(Widget child) {
return PipeWidget(parent: this, child: child);
}
}
extension WrapInWidget2<P extends Widget, W extends Widget>
on PipeWidget<P, W> {
PipeWidget<P, Widget> operator <<(Widget innerChild) {
return PipeWidget(
parent: parent,
child: PipeWidget(parent: child, child: innerChild),
);
}
}
Widget wrap(Widget p, Widget w) {
return switch (p) {
Center() => Center(child: w),
_ => throw UnsupportedError('Unsupported parent widget: ${p.runtimeType}'),
};
}
This makes Center() << Text() work. It is of course far from what you would want in the real world code, but I think it is an interesting exercise. We would hit some limitations (e.g. operators can't be generic), so maybe we can approximate with a method instead. wrap itself could be a method on a marker interface in the Widget hierarchy:
abstract interface class SingleChildWidget<W extends SingleChildWidget<W>> implements Widget {
W wrap(Widget child);
}
abstract interface class MultiChildWidget<W extends MultiChildWidget<W>> implements Widget {
W wrap(List<Widget> children);
}
I am aware that we could implement this with custom operators.
Although it's important to note that we still lose the const operator because of it.
IMO there's a lot of value in being able to write const Center(child: Text('foo')). This can drastically simplify performance optimisation of Flutter apps.
So being able to write const Center() < Text('foo') would be cool ; which isn't doable using custom operators.
There's also a major drawback to a custom operator: It wouldn't be backward compatible.
If we can write Center() < Tex(), we likely can't also write Center(child: Text()) if child is required. Meaning we have to make a breaking change to support such a syntax, or fork every existing widgets
FWIW as an alternative to const you can just move it to a static final field to achieve very similar effect.
If we can write
Center() < Text(), we likely can't also writeCenter(child: Text())ifchildis required. Meaning we have to make a breaking change to support such a syntax, or fork every existing widgets
That is true. You can still make it work by having a placeholder: final $ = const Placeholder();) and then Center(child: $) << Text().
Not that I really think you should write code like this. I am simply suggesting to use this as a vehicle for experimentation: it is relatively easy to concoct an approximation of proposed syntax from existing language features, so you can experiment with this syntax today. Maybe even ship it as a package. Better implementation would likely requires changes to Flutter framework, but I can also imagine some variants which don't.
I must be completely transparent that aesthetically this sort of syntax - no matter how it is implemented (be it a complicated concoction of extensions and helper classes or a language feature as proposed here) - does not really appeal to me in the slightest. It is too obscure and magical. Even more magical than Swift's result builders or whatever compiler plugin stuff Compose is doing.
Agreed. I started working on a proof-of-concept. I'll share something later today with custom operators. I have a pretty good solution
I must be completely transparent that aesthetically this sort of syntax - no matter how it is implemented (be it a complicated concoction of extensions and helper classes or a language feature as proposed here) - does not really appeal to me in the slightest. It is too obscure and magical. Even more magical than Swift's result builders or whatever compiler plugin stuff Compose is doing.
@mraleph I'm not sure I follow that particular critique of this proposal, how is it any more magical than this being available in an objects methods?