-
Notifications
You must be signed in to change notification settings - Fork 11
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
"child" and "children" add a lot of value #15
Comments
Yeah, for other non-"child" and "children" names, I would expect them to stay named. But from what I've heard from other Flutter users and even team members, the names "child" and "children" in particular are not worth their weight in screen real estate. The syntactic nesting itself implies a certain parent-child relationship. But, of course, ultimately it's up to you folks to design the APIs you think are best for your users based on the affordances the language provides.
Was the problem that it was unnamed, or that it was first? In Dart today, making a parameter positional also forces you to place it before the named arguments. You end up with code like: Container(
Row(
children: [
IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
),
Expanded(
child: title,
),
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
),
],
),
height: 56.0,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(color: Colors.blue[500]),
); That pushes This proposal allows you to have named arguments before positional ones specifically to solve that, so you could do: Container(
height: 56.0,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(color: Colors.blue[500]),
Row(
children: [
IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
),
Expanded(
child: title,
),
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
),
],
),
); |
Certainly requiring it to be first didn't help, but I don't think it was the main issue. It's been a while though. I agree that removing |
There actually is something special about However, I'm not arguing that rest args is the right solution for this problem. Rest args is a useful language feature, just not for ui-as-code. I think we should keep looking for a solution, one that's applicable beyond single-slot child models, and includes "home", "body", "leading" and so on, which are as special or as not special as |
Yes! One of the reasons I was interested in exploring a JSX-like syntax is because that notation gives you a very explicit visual distinction between attributes and children. Unfortunately, I was never able to come up with a syntax I liked that worked well with named single arguments ( I also spent a lot of time exploring a block-based syntax but couldn't figure out a syntax that I felt covered enough of the bases to be worth the additional complexity and switching costs. In particular, a block based syntax that still requires you to two sets of brackets to specify a list of children seems unsatisfactory to me: Row {
children: [
item,
item
]
} I noodled some on being able to hoist those out of the outer brackets: Row() children [
item,
item
] Syntactically, I think this might actually work. But I'm really worried it's just too weird. It's not totally unexplored territory. Grace supports something similar. It would take a lot of time to investigate this and see if something hangs together. There's a good chance it falls apart once you try to figure out how it interacts with things like method chains, subscript operators, etc. |
I wonder if we are thinking about child widget properties backwards. These properties indicate "slots" inside a parent, but at the technical level we chose to use parameters to express them. So this parameter bias keeps following us into all of our proposals. Let's consider the Scaffold widget. It has multiple slots, but let's focus on Currently you would use Scaffold(
body: Home(
...
),
persistentFooterButtons: [
FlatButton(
...
),
RaisedButton(
...
),
],
); While these slots are technically properties (passed as arguments), perhaps conceptually they are actually widgets, even though we do not have an explicit widget classes for them. This might sound weird, but in the HTML world (including Angular, JSX, etc) you might express this as: <scaffold>
<body>
<home>...</home>
</body>
<persistentFooterButtons>
<flat-button>...</flat-button>
<raised-button>...</raised-button>
</persistentFooterButtons>
</scaffold> So if HTML elements are like Flutter widgets, then "body" and "persistentFooterButtons" elements are also "widgets". If we syntactically translate this back to the block proposal, what we get is this: Scaffold {
body {
Home {
...
}
};
persistentFooterButtons {
FlatButton {
...
};
RaisedButton {
...
};
};
}; I admit, I have no idea how something like this can fit into the language. But I like how it looks! And the extra brackets are actually useful. |
amusingly, it was the result of exactly the opposite line of reasoning that led to where we are now. we started with html, then web components. in a world with aggressive composibility, there's really nothing special about a slot taking a particular type of widget, or a slot taking an arbitrary widget, or any other kind of argument. Flutter certainly commonly uses widgets, but there's no reason why you have to use them as your child of your widget... for example we sometimes use TextSpan objects as children. Indeed in Flutter Sprites you transition to an entirely different class hierarchy. A lot of our design is drawn from trying to avoid the design limitations that we saw on the web as a result of the approach you describe above. |
That's why I wrote "widgets", and not widgets :) They don't have to be instances of sub-classes of I think a language's job is not only to provide a sufficient number of nouns and verbs for developers to express logic, but also provide syntactic visual cues that can be used by libraries and frameworks to guide the developer when they read and try to understand code. For example, visual locality and isolation of related logic is very powerful for code understanding. That's why we have functions, closures and classes. Blocks simply extend that to anonymous non-reusable pieces of related logic (for example, initializing arguments, writing unit-tests), and they do that without any extra runtime cost. For example, they do not allocate objects. However, we definitely need to make sure there is place in the framework where new syntax actually provides useful guidance. That's something I haven't fully wrapped my head around. Widgets created inside Perhaps the defining characteristic of what should use the special syntax is when something takes up space on the UI (think layout), something you can point at on the screen, give it a name and that name will coincide with a name used in the code. Think |
I did toy with a syntax similar to what you have here, and also played around with something like: Scaffold {
body: Home {
...
}
persistentFooterButtons:
FlatButton {
...
};
RaisedButton {
...
};
}; It got weird pretty fast and I couldn't come up with anything that I felt held together. With your syntax, what are the actual semantics you have in mind? It seems like you use a postfix block to mean two different things in different contexts. In some places, it's an argument block where the name is a constructor/function and in others the name is a named parameter and the block is the list of values for it. Is that what you had in mind? How would the language reliably know which behavior you intend in any given context? |
For // create an empty structure (null)
json j;
// add an object inside the object
j["answer"]["everything"] = 42;
// add an array that is stored as std::vector (using an initializer list)
j["list"] = { 1, 0, 2 };
// add another object (using an initializer list of pairs)
j["object"] = { {"currency", "USD"}, {"value", 42.99} };
// instead, you could also write (which looks very similar normal JSON)
json j2 = {
{"pi", 3.141},
{"happy", true},
{"name", "Niels"},
{"nothing", nullptr},
{"answer", {
{"everything", 42}
}},
{"list", {1, 0, 2}},
{"object", {
{"currency", "USD"},
{"value", 42.99}
}}
}; (source)
Right. We already use
Haven't thought about it that far yet :) Borrowing from C++ initializer lists, the type of the LHS "expression" could be used. The language knows that |
That's true, but in those cases, we can syntactically distinguish what the use is.
That raises my hackles. Consider: foo {
bar {
baz
}
} If This means we rely on type information to parse. That way leads to madness,
Right, but my thinking is that if we have to do the control flow proposal anyway, we may as well use that for named arguments too: Scaffold(
if (blah) body:
Home(
...
)
),
persistentFooterButtons: [
FlatButton(
...
),
if (blah) RaisedButton(
...
)
]
}; The blocks don't really buy you much, at least not for Flutter's use case where you're just constructing an object. Where blocks would buy you something is in other DSLs where you're doing imperative work, like: sql.inTransaction {
var record = sql.query(...);
if (record.isSomething) {
sql.delete(...)
sql.insert(...)
} else {
sql.update(...)
}
sql.recalculateIndex();
} So I'm inclined to leave that syntax available so that we can use it for something more along these lines. I do like that you're pushing on this, and I admit cramming more stuff to into the existing parentheses-and-commas syntax isn't ideal. But doing an incremental refinement of that dramatically lowers the switching cost when users need a little conditional logic in an existing constructor call. I think it's overall a better fit for Flutter's need which is much more declarative (i.e. expression-based) than imperative (statement-based). |
+1 This is a very useful discussion. |
I posted it before in another thread, but this is the more appropriate place.
Of course, you can write any procedural logic in the initialization block, and with prefixes, it remains readable
In my opinion, this format is just "good enough" for the eye. Whether dart can accommodate it naturally is another issue, but I think it's just a minimal addition to the proposals discussed above. Except "child". For "child", we also need plus, and I don't know how to formally justify it for a single-valued parameter. EDIT 0: proposed block initialization syntax should be limited to constructors (as opposed to regular functions/methods), otherwise it may lead to confusion. E.g., the following should work, too
EDIT 1: for single-valued components like "child", we can simply choose another symbol - e.g. "*" instead of "+". What makes this syntax attractive is : not only the start of the element is easy to spot, but also - we have a clear vertical rhythm in the layout, by virtue of "+" (or "*") aligned vertically with the closing bracket of initialization block. I think this kind of vertical alignment is something that makes JSX so appealing to some users who otherwise hate XML with a passion, but the aesthetic value of vertical alignment is so strong, it makes it difficult to resist. |
In attempt to find a scientific justification for my "first character effect" claim, I did some research on internet and found this post. https://www.creativebloq.com/ux/how-human-eye-reads-website-111413463 The article claims that the first scan of the document is vertical: the eye is looking for the points of interest. I tried to conduct an experiment: opened several Flutter examples to see how I scan them. Indeed, I scan them vertically, and the points of interest (at least for me) are nested widgets. Attributes don't matter much at that point. At the first scan, my eye is focused only on the leftmost part of the code. But with the current way of presentation, the points of interest are hidden. There's too much noise in the code (esp. 'child' and 'children'), and nothing in particular really stands out. So the initial scan completely fails to identify the structure. If we are concerned about the ease of reading - then emphasizing the points of interest should be the first step IMO. The trick with plus and asterisk helps with that. But leading dots in the names of attributes - I'm not sure about them. If the idea of "initialization block" gets any traction, these dots might be good for disambiguation (in case of name collisions with the local variables), but on the other hand, they might create too many "points of interest". Is there anybody who likes the idea of asterisk and plus symbols? They are easy to explain: |
@tatumizer That is an intriguing idea. Certainly looking at your examples, despite my eyes untrained to such syntax, I can tell properties apart from child widgets. So I think the "first character" effect works. I bet if we add syntax highlighting it can be even clearer. Having said that, it does not seem to solve the "child" vs "children" vs "home" vs "persistentFooterButtons" problem, i.e. it does not have child slots that many widgets have. |
@yjbanov : Glad to hear that! :) (*) I'm not familiar with current Flutter taxonomy, have to make things up, sorry. |
Here's another (better?) idea:
As a consequence, the list types that require some other arguments (e.g. growable: true) will fit in the same template, with extra named parameter, e.g.
For "body", I'm not sure about its role, but: whether the asterisk is allowed or not, is determined based on special annotation (no general rule), so maybe "body" parameter can be annotated with "@enableAsteriskNotation", so we still get the same format
There are other possibilities, of course :) EDIT: forgot another PRO, very important one: this notation allows control flow statements (conditionals, loops etc) - the problem that was never addressed by XML, JSX or any other popular notation, so each "framework" had to come up with rather ugly and error-prone ways of working around this limitation. EDIT 1: Actually, the list of arguments in favor of "initialization blocks" would be quite long, but this thread is about child and children and other relatives, so I don't want to digress, just briefly outline other advantages: reducing the size of the program by eliminating intermediate variables (in many cases); increased locality; obviating the need in builder pattern (in many cases); creating immutable objects; etc. |
Along the lines of #17 (see comment by @yjbanov), class name List<Button> might be redundant in the initialization block (compiler already knows the type):
Which also can be generalized , so these 2 variants are equivalent:
is equivalent to
If we agree that the start of the line attracts more attention, then placing type Foo right at the start is preferable to (meaningless) "var" |
Child widgets are just properties. There is no meaningful distinction. For example, I have an app where one of my widgets takes a "child" that isn't a There's also the key difference between |
@Hixie: as our discussion progressed (above), the definitions gradually became more general, so it's not child vs children dichotomy any more. Instead, it's this: if your constructor has vararg parameter
Plus signs are important for reasons that are not logical, they are here to trigger some properties of the eye, which you need to be a neuroscientist to explain, and even that I'm not sure. Do you agree? :) EDIT: speaking about the "child", let's try to generalize it this way. Constructor has many parameters, most of them deal with fine-tuning, they are not structural. But there might be one important parameter that affects our understanding of the structure. If you can identify such parameter ("child" in one case, "body" in another), you can (it's up to you!) annotate it appropriately, which will enable asterisk notation. |
There's a few symbols that seem to work as well as plus. The best I found so far is a right-pointing triangle. Not sure whether (and how) text editors can support it, but this one on github displays it correctly
EDIT: there's a simpler idea, which keeps things backwards-compatible. See #21 for details. |
Leaving this here as a data point: https://devrant.com/rants/1897404/i-really-do-like-flutter-dart-but-i-just-cannot-be-the-only-who-thinks-that-the |
I may be in the minority here, but fwiw, I rather like the current syntax and think that the proposals I've seen thus far actually decrease readability and increase cognitive overhead. |
It's all subjective, but to fully appreciate new syntax, you have to see it in IDE, which shows vertical indent guides that get appropriately highlighted when you click on a section of your literal. Unfortunately, github markdown renderer doesn't show them. This may, or may not, convince you, but please give it a try. |
I am more concerned with what code looks like in a YouTube video or on a slide deck or a GitHub gist or a reddit post, than I am in what it looks like in an IDE. IDEs can always be made to mechanically convert whatever syntax we use to whatever syntax you prefer to make things Just Work. YouTube videos and the like can't be changed, and it's those where the readability is paramount (because that's what the people least familiar with the code will be looking at). |
I would not be a fan of this change. As soon as optional things start to depend on the order of definition, bad things happen for forward/backward compatibility. See, for example, flutter/engine#5275 (comment). If we could have additionally added named parameters to that it might have been slightly better, but all of the sudden it becomes a ton of cognitive overhead ("is this one of the ones I have to put in the right order or that I have to name?") - it really would have been better if they were all just named parameters to begin with, as you can then more easily and clearly add or remove/deprecate parameters as necessary for future revisions. This also means that I would generally look to avoid using "rest" parameters in exposed methods on the framework, except for maybe something like "hashValues" from It seems like it would be even worse to start trying to automagically determine which parameters are "content" and which are "attributes". I could see some value in having annotations for that, e.g.: class FooWidget extends StatelessWidget {
FooWidget({
@attribute this.color,
@attribute this.elevation,
@content this.child,
});
...
} Which could then be used by an IDE to provide more context/better rendering without harming the actual code that's written - and which, I think, is pretty clear when written properly and not nested too deeply (instead composing the tree from smaller functions/classes). This would also still allow framework authors freedom and latitude to determine what content is, or having multiple content parameters, etc. |
The distinction between "attribute" and "content" is a really loose one which I don't think makes much sense in Flutter anyway. |
I'm not yet fully convinced by this argument. To me it sounds like saying that the difference between a widget and render object is small because they are just objects. If something has a specific role and semantics, is frequently used, and code readability benefits from that thing to stand out in the code, it may be worth investigating specialized syntax for it. I see attributes vs content dichotomy similar to other precedents where language research concluded it is beneficial to use special syntax and semantics:
And yes, many UI toolkits have opted into providing two languages: Angular (code vs templates), Android/iOS (controllers vs layout XML), React.js (code vs HTML literals), Qt (code vs QML). I hope we don't end up in that situation, and instead solve the issue inside the main language. I think our developers want their UI layout code to stand out from the rest, and they also want a nice syntax for it. We just need to spend some time understanding what "UI layout code" is and how much code would fall into this category to benefit from whatever alternative syntax is offered. While I agree that rest arguments is not a great solution, I do believe the problem is real, and that we should invest in researching it. |
Although it can be a valid philosophical point, this distinction constitutes the basis of XML (and HTML) notation. Everybody got used to it already, so the issue is hardly controversial. IMO, It would be wise for dart to capitalize on this already well-developed intuition, rather than to fight against it. |
Maybe we reached the point where it would be worthwhile to consider some radical alternatives? Let's forget for a second about Flutter and widgets. Suppose are writing a program that simply computes something, but the calculations are complicated, so we have a function that depends on 10 parameters, each of the parameters in turn depending on other parameters, etc. My question is: how would you write the invocation of such function? Would it be a single, 50-line-long, deeply nested construct? Probably not. To keep things manageable, we need intermediate variables - perhaps lots of them, but that's the only way for a reader (and ourselves, too) to make sense of our program. The question I'd like to ask is: maybe in our quest for the best notation for Flutter literals we are barking the wrong tree? Maybe we are trying to solve a problem that doesn't really exist? We got fixated on monolithic literals whereas the solution is trivial: just decompose a complex thing into a number of simpler things? There are some examples of Flutter code of extraordinary complexity found in Bob's write-ups. Consider this code. Probably there's someone who can understand it, but it's not me. Faced with the perspective of maintaining such code, I would have no choice but to take it apart, defining one small thing at a time and gradually working my way up (in fact, I had to do that in real life while fixing problems in some clever javascript code). I tried to do the same for the Flutter snippet in question, and here's the sketch of what I've got:
The size of the program is about the same as the original. Most of the nested blocks are gone. Sub-components acquired recognizable names. What not to love here? (someone can prefer writing the functions in a different order: from bottom up instead of top down - this a matter of taste) The only downside I see is: we have to pass BuildContext around. Not sure this is a big deal. Someone can even argue it makes the dependency on BuildContext more explicit, which is a plus. But a huge advantage (other than reducing complexity) is that we can use control flow freely in any of these intermediate functions, which is very hard to achieve with literals. I don't know the reasons for Flutter tutorials to prefer single monolithic literals, can't speculate. But what if we just tell the truth instead: there's only so much complexity such literals can handle. Beyond certain point - we need decomposition. How about that? |
As an abstract exercise, if we were allowed to implement DSL, what would it look like? BEFORE:
AFTER:
EDIT: example with children that are not called "children" in API:
My theory is:
This is a raw idea so far, needs more work to accommodate some use cases. |
More complicated example: BEFORE:
AFTER:
NOTE: I tried to use ">" instead of "*", but it didn't work because the precedence of "+" is higher than ">", which makes ">" a non-starter. In the code above, I assumed that context can be passed automatically as $context, so there's no need to write an explicit builder. Does anyone like the notation? |
This is similar to Rust's generic bound constraint notation. I think this notation works when you want to declutter a long declaration or expression by moving non-essential details out of it. This works for generics because when you read a function declaration the first thing you usually want to parse out is the overall signature. However, Java-style generic syntax that inlines the generic bounds tends to obscure the signature. This makes it especially hard for new programmers to read. So I think this notation works there. However, widget properties are not clutter and their locality to the construction invocation improves code readability. If you need to inspect/change the |
You don't have to look especially hard to find the details: IDE will help you to jump to the right place. As with every notation, there are PROs and CONs. This notation addresses major pain points discussed in the write-up (poor utilization of horizontal space, proliferation of braces, poor readability in general). The trade-off is some modest amount of non-locality introduced in the code, which is quite normal in programming. I think the users would appreciate it, but it's hard to speculate without experimentation. |
Yes, but not Github, StackOverflow, code review tools, and many others.
I agree that excessive punctuation is a problem. However, I'd first verify if we can fix it with less drastic language changes, such as the optional semicolons proposal. |
Couldn't some of this just be solved by creating and following conventions? Perhaps with analyzer lints to help enforce them (e.g. a lint to check how deeply nested an in-line declaration is, or a lint to enforce that arrays not be specified inline, or perhaps even a configurable lint that takes types and parameter names and checks that they're done in or out of line). E.g. rather than: return SomeWidget(
helpfulInline: Colors.blue,
children: <Widget>[
AnotherWidget(...),
// more definitions
],
); Use final AnotherWidget another1 = AnotherWidget(...);
...
final List<AnotherWidget> listOfAnothers = [another1, ..., ..., ....];
return SomeWidget(
helfpulInline: Colors.blue,
children: listOfAnothers,
); |
More arguments in favor of where-notation. Where-notation is all over the place in math and physics. If you open any textbook or wiki article, all you will see is where-notation. Where-notation improves locality (@yjbanov: I'm countering your argument). Where-statements can be nested, or flattened - depending on your stylistic preferences.
Or in flat form:
(Though probably the former is better, exactly due to locality). IMPORTANT: the concept applies to any computation, not limited to Flutter UI. |
Is there an issue for making child and children positional ? Records could be used for the "slot" issue, when there are multiple slots instead of one child / children // slots
Scaffold(
color: x,
background: y,
(
body: Body(),
floatingActionButton: FloatingActionButton(),
)
)
// child
Center(
Text('hi')
)
// children
Row(
mainAxisSize: .spaceBetween,
[
Text('hi')
SizedBox(),
]
); Slots (Widgets and descendant) should also be more visually emphasized, with color highlight, than attributes. If widgets were to always be the positional arguments and attributes were to always be the named parameters there would at least be some conventional ground for highlighting. That is except for widgets that do not have a child like
This would still hold, except |
This is a clever idea! I'll think about this more. :) |
There's an assertion in https://github.com/munificent/ui-as-code/blob/master/in-progress/parameter-freedom.md that the "child" and "children" named arguments should become unnamed because they don't add value.
There is nothing special about "child" or "children"; not all widgets use those names, e.g. MaterialApp (the first widget people come across) uses "home", while Scaffold and AppBar (the next two predefined widgets they come across) use "body" and "appbar" and "leading" and "title" and so on.
Having the clear distinction of "child" and "children", and of other common names like "builder", is important to convey how many children a widget takes, what role the child has, how it should be expressed (e.g. static widget vs closure), and to make a clear link from the parameter in the constructor to the property on the widget. It also makes it much easier to reference the parameter in documentation.
In conclusion, I do not believe we would make that change even if the language made it easy to do so. We have in the past experimented with using unnamed arguments (e.g. for a while the child was always the first argument, unnamed), and we moved away from this very purposefully.
The text was updated successfully, but these errors were encountered: