-
Notifications
You must be signed in to change notification settings - Fork 56
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
field and method chains #66
Comments
(I'm going to try something different and specify this across multiple comments, hopefully to make things easier to comment on). Simple casesIf the whole thing fits on one line, do that. E.g.,
If it does not, split across multiple lines, one line for each element of the chain. Block indent, start lines with dots, newline after the chain root. E.g.,
|
@nrc That seems right for simple cases. Most complex cases would involve breaking one of the individual components across lines. |
I am fond of breaking only the last method call without breaking the earlier methods to save vertical space when only the last method call is wide. However, I'm not really sure how to codify the rule. Example 1, to save you a click: let global_value = self.globals.get_mut(&id)
.expect("global should have been cached (static)"); A more naive rule would give me this less desirable result: let global_value = self
.globals
.get_mut(&id)
.expect("global should have been cached (static)"); |
After looking at it for a bit, I don't entirely mind what I called the naive rule, but it's not what I write naturally and it reduces the amount of code I can fit on my screen (I consider it valuable to be able to see more at once without scrolling back and forth). |
Multiple line componentsIf any component is multi-line, then that forces the multiline form from the above comment. Sub-expressions should be further indented (i.e., as if the left margin were one block indent). E.g.,:
Last element exceptionThe last element in the chain may go multiline, without forcing the rest of the chain to be multiline, e.g.,
|
I prefer to avoid the special case in #66 (comment), I don't mind the extra vertical space in the example you give. |
@nrc I'd suggest phrasing the general rule as "If any component other than the last is multi-line, ..." |
For the sake of considering alternatives (without necessarily advocating this style), I've also seen code that doesn't add the newline after the asdfasdf
.foo
.bar(
argument1,
argument2,
argument3,
).baz(
bazargument,
); I've seen cases where this improves code, and cases where it doesn't; I don't know that I could articulate a general rule for when it makes sense. |
@nrc I would advocate a slightly different rule. You can break a component other than the last across multiple lines, but you must break every component after that one as well. You can, however, keep all the components before that first break on the same line. So, for instance: x = foo.short().short2(arg)
.long_name(argument)
.short()
.longer_name(
argument1,
argument2,
)
.still_longer_name(); Notice that the |
@joshtriplett Nice, that's a pretty understandable rule. I could go either way about having That is, I like this: let x = foo.field.subfield
.long_name(argument)
.short()
.longer_name(
argument1,
argument2,
); And I dislike this: let x = foo
.field
.subfield
.long_name(argument)
.short()
.longer_name(
argument1,
argument2,
); It feels excessive to have a line just for a field access, and I'm comfortable treating short method calls the same way to keep the rule simple. Not to mention it deals with the previous case I mentioned the way I like. |
@solson As long as the rule doesn't allow |
I don't like keeping the first line on a single line if later lines are broken. So I'd rather see: x = foo
.short()
.short2(arg)
.long_name(argument)
.short()
.longer_name(
argument1,
argument2,
)
.still_longer_name(); The ability to easily scan down the chain of calls is preferable to me than saving the initial few lines. It's not something that I feel strongly enough to argue about whether or not it should be the default formatting, but it would be nice if there was an option to configure it. |
I agree with @casey it's much easier for me to read the text if I don't have to do both horizontal and vertical scanning. for this reason - in general my preference is that once a switch is made in a block of text ( |
I agree with #66 (comment) and #66 (comment) but I also like to go one step further in my own code: let result_with_long_name: Option<SomeType> =
source // ← on a new line, makes parsing the chain as one block easy; would like to see this at least if there are ≥40 chars (incl. indentation) in front of the `=`
.field
.map(Thingy::do_stuff)?
.lorem(|x| if x.is_good() { x } else { Thingy::default() })
.ipsum(|thingy| {
thingy // ← using a one-letter binding looks bad here, which will remind you to make your code readable ;)
.foo() // ← new line
.parse()
.unwrap_or(42)
})
.dolor(
sit,
amet,
<Consetetur as Sadipscing>::elitr(), // ← trailing commas EVERYWHERE
)
; // ← not necessarily on a new line but reduces diff churn Just wanted to let you know my preference, as I usually write long chains :) |
I also agree with @casey - I prefer not to allow a dense first line. I think there is a huge advantage to being able to scan down a chain once one is being used. |
I would like to propose a tweak to my simple rules. If the first item width is <= the tab width, then the second item may go on the same line. E.g.,
After implementing, I found the 'orphan's formed by my initial rule to be annoying. |
Complex chainsIf a chain fits on one line, but is not short (see #47), then it should be split across multiple lines. |
Try operatorThe try operater (
or in the single line case:
|
An edge caseIf the parent is multi-line and ends with
We could allow the first child only on to the same line as
We could double indent the parent element (this is visually a bit inconsistent because how a call is formatted depends on what comes next:
Also note that now |
Alternatively, we could not increase the indentation level for the chain at all if the parent ends with a closing bracket: given(
weirdly_long_arg1,
arg2,
)
.running(waltz)
.foo We might differentiate that depending of the context of the chain. The example above is free-standing, but if it were in a binding, I'd try to put it in its own visual block, indented by one level: let result_thingy =
given(
arg1,
arg2,
)
.running(waltz)
.foo; |
@nrc I would favor the approach of writing |
I want to leave my opinion here. I was working on a really long chain and I came up with a style that I like: pub fn number_of_conflicting_queens(&self) -> usize {
let same_line: usize = self.board
.iter()
.enumerate()
.take(self.len() - 1)
.map(|(i, queen)| self.board
.iter()
.skip(i + 1)
.filter(|&other| queen == other)
.count())
.sum();
let diagonals: usize = self.board
.iter()
.enumerate()
.map(|(cola, rowa)| self.board
.iter()
.enumerate()
.skip(cola + 1)
.filter(|&(colb, rowb)| difference(*rowa, *rowb) == difference(cola, colb))
.count())
.sum();
return same_line + diagonals;
} Basically in closures, the root object says in the same line as the closure parameters, and every method is indented by one. That gives it a sense of "I'm chaining on something else here" while only increasing indentation by 1. It looks more aesthetically pleasing than pub fn number_of_conflicting_queens(&self) -> usize {
let same_line: usize = self.board
.iter()
.enumerate()
.take(self.len() - 1)
.map(|(i, queen)| {
self.board
.iter()
.skip(i + 1)
.filter(|&other| queen == other)
.count()
})
.sum();
let diagonals: usize = self.board
.iter()
.enumerate()
.map(|(cola, rowa)| {
self.board
.iter()
.enumerate()
.skip(cola + 1)
.filter(|&(colb, rowb)| difference(*rowa, *rowb) == difference(cola, colb))
.count()
})
.sum();
return same_line + diagonals;
} Here each chain increases indentation by 2, which is really ugly and space wasting. The only "benefit" is that it makes the object being chained more obvious, but that's not a problem in the previous suggestion, I don't think. |
Summary for FCP (more details in various comments above):
|
I'm having second thoughts about the last line/multiline exception, e.g., this change:
that seems to make things worse. When reading this code, the original is easy to scan the important things by scanning the newlines, whereas in the formatted version, the important stuff is all bunched together and less important arguments are easy to scan. |
I've noticed what I think is the same problem: let mybaz = foo.bar().baz().expect("error getting baz"); Turned into: let mybaz = foo.bar().baz().expect(
"error getting baz"
); Makes the |
@nrc I think no matter which option we pick, we'll end up with cases where it produces a result we don't like; I can think of cases where doing it the other way around would produce a poor result as well. However, I do agree that the case you posted works better with the whole Would it require too much search in rustfmt to say something like "if breaking the method chain across lines would leave the whole call on one line, do that, otherwise break the call and keep the method chain on one line if that's not too wide, otherwise break both"? Also, would dropping the rule of "If a chain fits on one line, but is not short (see #47), then it should be split across multiple lines" make your example format better? |
I think this would be ok
I don't think it helps this example. In general it would mean it affects fewer cases. |
Could you clarify the foo(
...
).map(|| { // edge case, start on the line with `)`
...
}).filter(|| { // should we start with on the same line here as well?
...
}).flat_map(|| { // and here?
}) |
I've encountered some issues with the "edge-case" of a multi-line parent followed by one or more children, as referenced by @matklad above: foo(
....
).map_err(|err| ......)
.and_then(|val| ...)
....
// contrast with
foo(
....
).map_err(|err| ........
.and_then(|val| ...)
.... Notice how the second block misses the final parenthesis of the foo(
....
)
.map_err(|err| ......)
.and_then(|val| ...)
.... |
I found the following pattern to be common when writing a method chain in rust: items.iter().map(|item| {
// ...
item.foo().bar()
}).collect() that is, items.iter()
.map(|item| {
// ...
item.foo().bar()
})
.collect() which, IMHO, is worse than the former. Is it reasonable to capture the above pattern and use the former style for it? |
Can you try to explain why? I actually find the second form easier to read. |
I'd usually format it like this: items
.iter()
.map(|item| {
// ...
item.foo().bar()
})
.collect() All chained methods are aligned so that I can have a visual cue about what chained methods are being applied to an object all in one go. |
I can see some benefit of the former. It doesn't have rightward drift and it isn't too fat vertically. However, if once there are two calls in the chain that have function calls, it gets uglier much quicker IMO. |
e.g.,
a.foo.bar(x).baz()
The text was updated successfully, but these errors were encountered: