munificent / ui-as-code

Design work on improving Dart's syntax for UI code
BSD 3-Clause "New" or "Revised" License
121 stars 11 forks source link

"child" and "children" add a lot of value #15

Open Hixie opened 6 years ago

Hixie commented 6 years ago

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.

munificent commented 6 years ago

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.

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.

(e.g. for a while the child was always the first argument, unnamed), and we moved away from this very purposefully.

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 height, padding, etc. very far away from the name of the widget they apply to.

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',
      ),
    ],
  ),
);
Hixie commented 6 years ago

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 child and children is a common piece of feedback, and it makes sense to explore what that would enable. The reason I don't think we'd do that change overall is that while it seems to make sense in some common cases, it makes the overall design less coherent and predictable, and has downstream effects (such as those on the documentation) that are subtle and easily overlooked but nonetheless IMHO quite detrimental to the overall health and usability of the API.

yjbanov commented 6 years ago

There actually is something special about child and children, but it's not a technical feature, but how these arguments are used. Namely, they predominantly come as the last argument in the argument list. This likely means that in developers' minds child widgets are conceptually separate from "properties", so they put them last. This holds even in Flutter's own code.

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 child and children are.

munificent commented 6 years ago

This likely means that in developers' minds child widgets are conceptually separate from "properties", so they put them last.

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 (child, body, etc.), named collection arguments (children, actions, etc.), and positional 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.

yjbanov commented 6 years ago

a block based syntax that still requires you to two sets of brackets to specify a list of children seems unsatisfactory to me

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 body and persistentFooterButtons for a moment.

Currently you would use Scaffold like this:

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.

Hixie commented 6 years ago

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.

yjbanov commented 6 years ago

That's why I wrote "widgets", and not widgets :) They don't have to be instances of sub-classes of Widget. So TextSpan would qualify. I'm also not trying to argue that HTML as a whole is good UI language. It just happened to work for this particular case. HTML attaches a lot of undesired semantics to that syntax, in addition to the semantics that developers and framework authors want, and there's plenty that it can't do that developers want it to, leading to awkwardness (see Angular's "directives" and the numerous annotations for binding to HTML). There are plenty of non-HTML examples btw, such as Anko - a DSL for Android layouts for Kotlin, or QML that uses block syntax not just for building widget hierarchies, but also for state machines.

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 build methods are one obvious place. But that's not enough. It should be applicable in more places. Hence the idea above.

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 Padding, FlatButton, children, home, TextSpan, builder. I would also expect it to be something that has a lifespan. It's not just a paint operation, like drawRect, but either an object by itself (e.g. a widget), or owned by an object (e.g. a property of a widget). So the visual cue of the name { } brackets implies a UI building block, and sets it apart from name() and name =.

munificent commented 6 years ago

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?

yjbanov commented 6 years ago

With your syntax, what are the actual semantics you have in mind?

For Scaffold { ... } it's the AIB as in the current proposal. For body { ... } and persistentFooterButtons { ... } the closest thing I can think of is initializer lists from C++. For example, the nlohmann JSON library uses them as DSL for expressing JSON:

// 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)

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.

Right. We already use { } for all kinds of things: classes, maps, named parameters, methods, functions, switch statements, etc. This just continues the trend.

How would the language reliably know which behavior you intend in any given context?

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 body is a parameter of type Widget which inside an AIB can only be assigned to, therefore body { ... } can be treated as assignment. persistentFooterButtons is the same, except its a List and so the language treats persistentFooterButtons as a "list initializing block". Obviously, we'll need to introduce this "list initializing block" in the first place. I'm guessing it will differ from the normal "block", as in, it's not a list of statements (otherwise we'll need something like yield, which is a no go). Perhaps a list of expressions is sufficient, if it can also be supported by the control flow proposal. Optional semi-colons and optional commas could help here so users do not have to think about the different kinds of punctuation.

munificent commented 6 years ago

Right. We already use { } for all kinds of things: classes, maps, named parameters, methods, functions, switch statements, etc. This just continues the trend.

That's true, but in those cases, we can syntactically distinguish what the use is.

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 body is a parameter of type Widget which inside an AIB can only be assigned to, therefore body { ... } can be treated as assignment.

That raises my hackles.

Consider:

foo {
  bar {
    baz
  }
}

If bar is a function, then baz is parsed as an expression statement. But if bar is a named parameter of foo, then baz is an implicitly yielded value passed to bar.

This means we rely on type information to parse. That way leads to madness, especially in Dart where identifies may be imported from other libraries.

Obviously, we'll need to introduce this "list initializing block" in the first place. I'm guessing it will differ from the normal "block", as in, it's not a list of statements (otherwise we'll need something like yield, which is a no go). Perhaps a list of expressions is sufficient, if it can also be supported by the control flow proposal.

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).

leafpetersen commented 6 years ago

I do like that you're pushing on this

+1

This is a very useful discussion.

tatumizer commented 6 years ago

I posted it before in another thread, but this is the more appropriate place. The main idea (or conjecture, if you will) is that while scanning the line, our eye gives disproportionate attention to the first character. I would say, extremely disproportionate. Unfortunately, I don't know how to measure it exactly, but let's assume this is true, just for the sake of argument. What immediately follows is that while writing parameters/children in these literals, we need a good first character that triggers some neurons in the brain, so to speak.

Card {
  .color= Colors.white;
  +Center {
    +Column {
      .mainAxisSize = MainAxisSize.min;
      .crossAxisAlignment = CrossAxisAlignment.center;
      +Icon(choice.icon, size: 128.0, color: textStyle.color);
      +Text(choice.title, style: textStyle);
    }
  }
}

Of course, you can write any procedural logic in the initialization block, and with prefixes, it remains readable

Card {
  .color= Colors.white;
  +Center {
    +Column {
      .mainAxisSize = MainAxisSize.min;
      .crossAxisAlignment = CrossAxisAlignment.center;
      +Icon(choice.icon, size: 128.0, color: textStyle.color);
      for (int i=0; i<10; i++) {
        +Text("line${i}", style: textStyle);
      }
    }
  }
}

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

    //...
    Column {
      .mainAxisSize = MainAxisSize.min;
      .crossAxisAlignment = CrossAxisAlignment.center;
      +Icon(choice.icon, size: 128.0, color: textStyle.color);
      [1, 2, 3].forEach( (n) =>
        // we are still in the context of Column init block, not in the context of "forEach" method
        +Text("line${n}", style: textStyle); 
      );
    }

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.

tatumizer commented 6 years ago

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: \*X is simply a shortcut for child=X, and +X - shortcut for children.add(X). There;s nothing magic about the names child/children - it can be any attribute, appropriately annotated.

yjbanov commented 6 years ago

@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.

tatumizer commented 6 years ago

@yjbanov : Glad to hear that! :) For persistentFooterButtons and other things that don't fit, let's take a hammer and make it fit :) E.g. persistentFooterButtons may be redefined as WidgetContainer\

tatumizer commented 6 years ago

Here's another (better?) idea:

  1. retrofit every List with a constructor accepting vararg parameter "...items"
  2. general rule: for every vararg parameter, by default allow "+" notation in the initialization block, so the declaration will look like
Scaffold {
  body: Home {
    ...
  }
  persistentFooterButtons: List<Button> {
    +FlatButton {
     // ...
    }
    +RaisedButton {
      //...
    }
  }
}

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.

var foo = List<Button> {
  growable = true
  +FlatButton {
     //...
  }
  +RaisedButton {
     //...
  }
}

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

Scaffold {
  *Home {
    ...
  }
  persistentFooterButtons = List<Button> {
    +FlatButton {
     // ...
    }
    +RaisedButton {
      //...
    }
  }
}

There are other possibilities, of course :) What's important is that the format eliminates the noise almost entirely: if the proposal of optional semicolons is accepted, the above notation will be completely free of semicolons and commas; instead of 3 types of brackets (..), [...] and {...} we will have only one {...}; we replace identifiers like child and children with easily recognizable symbols... I think the users will appreciate it :)

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.

tatumizer commented 6 years ago

Along the lines of #17 (see comment by @yjbanov), class name List\

Hixie commented 6 years ago

I can tell properties apart from child widgets

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 Widget, it's a data model, and it generates the right Widget out of it. Another widget in that app takes another Widget as a child but never actually places it in the tree. Another takes multiple widgets and builders and combines them in various ways with the other non-widget, non-builder properties.

There's also the key difference between child, children, sliver, and slivers which would be completely lost if we removed the name here.

tatumizer commented 6 years ago

@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 List<Sliver> ...slivers , it allows the syntax with the "plus" sign in the initialization block. Maybe it needs a special annotation for that, But the name of the parameter can be anything. The alternative is to declare it simply as a list (not vararg) named parameter List<Sliver> slivers, but then, the List itself, being retrofitted with vararg parameter, allows the notation like this:

Foo {
   //...
  slivers = {
    +Sliver {
        //..
    }
    +Sliver {
      //..
    }
  } 
}

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. "There are more things in heaven and earth, Horatio, Than are dreamt of in your philosophy" (Hamlet 1.5.167-8)

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. BTW, I have second thoughts about asterisk again, maybe "plus" would be better, despite having no "add" semantics in case of single-valued parameter. It's mostly for the eye. We may conduct an experiment right here. Please send me the most complicated example of the literal you know, and we can compare BEFORE and AFTER.

tatumizer commented 6 years ago

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

Card {
  color= Colors.white;
  ▷Center {
    ▷Column {
      mainAxisSize = MainAxisSize.min;
      crossAxisAlignment = CrossAxisAlignment.center;
      ▷Icon(choice.icon, size: 128.0, color: textStyle.color);
      ▷Text("line${i}", style: textStyle);
    }
  }
}

EDIT: there's a simpler idea, which keeps things backwards-compatible. See #21 for details.

munificent commented 5 years ago

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

tvolkert commented 5 years ago

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.

tatumizer commented 5 years ago

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.

Hixie commented 5 years ago

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).

dnfield commented 5 years ago

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, https://github.com/flutter/engine/pull/5275#discussion_r188770256. 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 dart:ui.

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.

Hixie commented 5 years ago

The distinction between "attribute" and "content" is a really loose one which I don't think makes much sense in Flutter anyway.

yjbanov commented 5 years ago

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.

tatumizer commented 5 years ago

The distinction between "attribute" and "content" is a really loose one which I don't think makes much sense in Flutter anyway.

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. It's just a matter of HOW :)

tatumizer commented 5 years ago

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:

var cupertinoTabScaffold = CupertinoTabScaffold(
   tabBuilder: (BuildContext context, int index) => cupertinoTabView(context, index)
);

cupertinoTabView(BuildContext context, int index) => CupertinoTabView (
  builder: (BuildContext context, int index) => cupertinoPageScaffold1(context, index)
);

cupertinoPageScaffold1(BuildContext context, int index) => CupertinoPageScaffold(
  navigationBar: CupertinoNavigationBar(
    middle: Text('Page 1 of tab $index')
  ),
  child: nextPageButton(context, index)
);

nextPageButton(BuildContext context, int index) => Center(
  child: CupertinoButton(
    child: const Text('Next page'),
    onPressed: () => onNextButtonPressed(context, index),
  )
);            

onNextButtonPressed(BuildContext context, int index) => Navigator.of(context).push(
  CupertinoPageRoute<Null>(
    builder: (BuildContext context) => cupertinoPageScaffold2(context, index)
  )  
);

cupertinoPageScaffold2 (BuildContext context, int index) => CupertinoPageScaffold(
  navigationBar: CupertinoNavigationBar(middle: Text('Page 2 of tab $index')),
  child: backButton(context)
);

backButton(BuildContext context) => Center(
  child: CupertinoButton(
    child: const Text('Back'),
    onPressed: () => Navigator.of(context).pop()
  )
);

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?

tatumizer commented 5 years ago

As an abstract exercise, if we were allowed to implement DSL, what would it look like? Here's an (original?) idea: to write the structure of the widget in a manner that lends itself to human understanding, we need to first create a concise bird's-eye view, representing only the child-parent and sibling relations. Then we can fill the details. Relation of "A is a parent of B" can be represented as multiplication A*B; Relation of "X is a sibling of Y" - as a sum X + Y. Example:

BEFORE:

Card (
  color: Colors.white,
  child: Center (
    child: Column (
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Icon(choice.icon, size: 128.0, color: textStyle.color),
        Text(choice.title, style: textStyle)
      ] 
    )
  )
)

AFTER:

Card card * Center * Column column * (Icon icon + Text text) where
   card = (color: Colors.white),
   column = (minAxisSize : MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center),
   icon = (choice.icon, size:128.00, color: textStyle.color),
   text = (choice.title, style: textStyle)

EDIT: example with children that are not called "children" in API:

Scaffold * (Home h + persistentFooterButtons(FlatButton fb + RaisedButton rb)) where
   h = (..).
   fb = (...),
   rb = (...)

My theory is:

  1. The readability of the expression is higher than that of any of the popular alternatives (including JSX)
  2. Adding/removing components is trivial (in contrast with the current notation)
  3. Notation solves the hard problem of Flutter: horizontal space utilization

This is a raw idea so far, needs more work to accommodate some use cases.

tatumizer commented 5 years ago

More complicated example:

BEFORE:

CupertinoTabScaffold(
  tabBuilder: (BuildContext context, int index) {
    return CupertinoTabView(
      builder: (BuildContext context) {
        return CupertinoPageScaffold(
          navigationBar: CupertinoNavigationBar(
            middle: Text('Page 1 of tab $index'),
          ),
          child: Center(
            child: CupertinoButton(
              child: const Text('Next page'),
              onPressed: () {
                Navigator.of(context).push(
                  CupertinoPageRoute<Null>(
                    builder: (BuildContext context) {
                      return CupertinoPageScaffold(
                        navigationBar: CupertinoNavigationBar(
                          middle: Text('Page 2 of tab $index'),
                        ),
                        child: Center(
                          child: CupertinoButton(
                            child: const Text('Back'),
                            onPressed: () { Navigator.of(context).pop(); },
                          ),
                        ),
                      );
                    },
                  ),
                );
              },
            ),
          ),
        );
      },
    );
  },
)

AFTER:

@DSL CupertinoTabScaffold * CupertinoTabView * CupertinoPageScaffold * 
  (navigationBar(CupertinoNavigationBar bar1) + Center * CupertinoButton btn1 * Text nextPage)
where 
  bar1 = (middle: Text('Page 1')), 
  btn1 = (
    onPressed: () => () {
      Navigator.of($context).push(
        @DSL CupertinoPageRoute<void> * CupertionPageScaffold * 
          (navigationBar(CupertinoNavigationBar bar2) + Center * CupertinoButton btn2 * Text prevPage)
        )
    }
  ),
  bar2 = (middle: Text('Page 2')),   
  btn2 = (onPressed: () { Navigator.of($context).pop(); },
  nextPage = ("Next page"), 
  prevPage = ("Back"),

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. Not sure this assumtion is correct though.

Does anyone like the notation?

yjbanov commented 5 years ago

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 middle or onPressed property you don't have to look hard to find it.

tatumizer commented 5 years ago

You don't have to look especially hard to find the details: IDE will help you to jump to the right place. Speaking of clutter: the whole point of the notation is to get rid of a real clutter - overwhelming amount of punctuation (different types of braces, parens etc.).

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.

yjbanov commented 5 years ago

IDE will help you to jump to the right place.

Yes, but not Github, StackOverflow, code review tools, and many others.

real clutter - overwhelming amount of punctuation (different types of braces, parens etc.)

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.

dnfield commented 5 years ago

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,
);
tatumizer commented 5 years ago

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. This notation is a result of long evolution of the style of writing (it was not always like that). The fact that the programming languages don't follow it is a historical accident IMO, probably having more to do with the structure of Fortran programs and punch cards than with the logic or readability.

Where-notation improves locality (@yjbanov: I'm countering your argument). Where-statements can be nested, or flattened - depending on your stylistic preferences. E.g.

z = f(x, y) where {
   x = ...
   y = g(z, h) where {
      z = ...
      h = ...
   }
}

Or in flat form:

z = f(x, y) where {
   x = ...
   y = g(z, h)
   z = ...
   h = ...
}

(Though probably the former is better, exactly due to locality).

IMPORTANT: the concept applies to any computation, not limited to Flutter UI. Without where-notation, one would need to introduce lots of articicial functions like computeZ(), computeH(), cluttering the program with noise. In other words, it's not a DSL. But the concept needs to be developed further, it has a lot of potential - dart has a chance to distinguish itself from the competition here. Anyone willing to discuss?

cedvdb commented 8 months ago

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 Text

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

This would still hold, except child and children would be implicit.

munificent commented 8 months ago

Records could be used for the "slot" issue, when there are multiple slots instead of one child / children

This is a clever idea! I'll think about this more. :)