fsharp / fsharp/fslang-design

Laying out an infix expression that does not fit on one line

Open
#836 5 comments 0 reactions 0 assignees View on GitHub
style-guide
Dominant language
F#
Stars
557
Forks
149
Avg merge
4d 13h
Merged PRs (30d)
2

Description

#### Is your request about [official style guide](https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/) update?

Style guide update adoption checklist:
* [ ] I have submitted a PR / Draft on docs repository
* [ ] A PR was merged about it on docs repository and I want to bring it to attention of the community
* [x] I want the community to debate and share perspectives
* [ ] This is higher level concern than formatting:
* [ ] soundness/robustness of F# code
* [ ] performance concerns
* [ ] approachability of code towards:
* [ ] people debutting with programming languages
* [ ] seasoned developers

---

# Laying out an infix expression that does not fit on one line

## The problem

Code is written to a maximum line length. Given one to respect, an infix expression `a = b` can be
too long to keep on a single line, and then part of it has to move down.

At a maximum of 80, this does not fit:

```fsharp
let v = xs = [ "aaaaaaaaaa"; "bbbbbbbbbb"; "cccccccccc"; "dddddddddd"; "eeeeeeeeee" ]
```

Code shaped like this usually wants one of its two sides pulled out into a binding of its own:

```fsharp
let expected = [ "aaaaaaaaaa"; "bbbbbbbbbb"; "cccccccccc"; "dddddddddd"; "eeeeeeeeee" ]
let v = xs = expected
```

Good advice, and beside the point here. Expressions of this shape are legal and something has to be
printed for them, so the note takes the one above as given.

There are three parts that could move: the left-hand side, the operator, and the right-hand side.
The left-hand side is where the expression begins, so it stays; what follows works out where the
other two can go, what a good answer has to achieve, and which of the layouts that remain is worth
adopting.

Every sample below was produced by formatting a real input at a maximum of 80, and every claim about
what does and does not compile was checked by parsing the output back. How, and on what, is at the
end.

Three markers appear in the samples. ✅ is what the note is proposing. ⛔ is a layout it rejects,
because it breaks one of the requirements set out below. ⚠️ is a layout that passes every check that
can be automated and is still wrong, which is the harder case to see and the reason the columns are
spelled out.

## The operators this is about

Five of them: `=`, `>`, `<`, `%` and `%%`. Every other infix operator moves down with the right-hand
side and starts the new line, which settles the question for them:

```fsharp
let v =
someCollection
|> List.map someFunction
|> List.filter someOtherFunction
|> List.sum
```

These five cannot do that, because at the column of the left-hand side the parser reads them as
something else. For `=`, `>` and `<` it is the `=` of a binding, and the result does not compile:

```fsharp
let v =
someLongThing
= otherThing

// (3,5): error FS0010: Unexpected symbol '=' in binding. Expected incomplete
// structured construct at or before this point or other token.
```

```fsharp
let v =
someLongThing
< otherThing

// (3,5): error FS0010: Unexpected symbol '<' in binding. Expected incomplete
// structured construct at or before this point or other token.
```

For `%` and `%%` it is worse, because the result compiles and means something else. At the column of
the left-hand side, inside a quotation, a `%` is a splice:

```fsharp
let v =
<@
someLongThing
% otherThing
@>

// no error, but `% otherThing` has become the splice `%otherThing`
```

That is the whole of the constraint, and it is narrower than it looks. Indent the operator one level
past the left-hand side and all five are fine, the `%` included:

```fsharp
let v =
someLongThing
= otherThing

// parses, and `%` at this column stays infix rather than becoming a splice
```

So one thing is settled and not by preference: **nothing belongs at the column of the left-hand
side except the left-hand side.** Where the operator and the right-hand side go beyond that is open.

## What a good answer has to achieve

Four things.

### 1. Telling the two sides apart

```fsharp
let v = xs.ReplaceEverythingEverywhere(11111, 22222).ReplaceEverythingEverywhere(33333, 44444).TrimEnd() = expected.ReplaceEverythingEverywhere(55555, 66666).ReplaceEverythingEverywhere(77777, 88888).TrimEnd()
```

One `=`, with a chain of three calls on `xs` to its left and a chain of three on `expected` to its
right. Give the right-hand side nothing of its own and the two merge:

```fsharp
let v =
xs
.ReplaceEverythingEverywhere(11111, 22222)
.ReplaceEverythingEverywhere(33333, 44444)
.TrimEnd() = expected
.ReplaceEverythingEverywhere(55555, 66666)
.ReplaceEverythingEverywhere(77777, 88888)
.TrimEnd()
```

Six lines beginning with a dot, all at one column, and nothing to say that the first three belong to
`xs` and the last three to `expected`. The only thing separating the halves is `expected` at the
end of a line between two dotted lines, which is easy to miss. An application or a lambda on the
right-hand side has the same problem for the same reason.

### 2. Not spending a line on a short right-hand side

```fsharp
let v = xs aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccc = 0
```

The whole expression is too long, but the right-hand side is `0`. Once the application breaks there
is room for it beside the operator, and it should stay there:

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc = 0
```

rather than take a line to itself:

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc =
0

// ⛔ a line spent on `0`, with the rest of that line left empty
```

### 3. The left-hand side's length must not decide the right-hand side's layout

Take a `match` and change nothing but the name on the left:

```fsharp
let v = xs = match yyyyyyyyyyyyyyyyy with | Aaaaaaaaaaaaaaaaaaaa -> 1 | Bbbbbbbbbb -> 2
let v = theComparisonResultValue = match yyyyyyyyyyyyyyyyy with | Aaaaaaaaaaaaaaaaaaaa -> 1 | Bbbbbbbbbb -> 2
```

Place the right-hand side where the operator happened to end and every arm moves with it:

```fsharp
let v =
xs = match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
```

```fsharp
let v =
theComparisonResultValue = match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
```

A rename that has nothing to do with the expression reflows every line of it. A `try ... with`, a
record, a copy-and-update record, an object expression, an `if` and a tuple all do the same. A list,
an application and a chain do not, because they indent from a fixed level.

### 4. Every column a multiple of the indent size

A construct that places its contents under its own opening token lands wherever that token happens
to sit. That column was chosen by nothing: it is the width of whatever came before it. Put a record
beside the operator, as the previous requirement's `match` was, and its fields land at column 13 and
its closing brace at 9, neither of which is a multiple of four:

```fsharp
let v =
xs = {
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}

// ⛔ fields at 13, closing brace at 9
```

On the grid, the same record reads the way a record reads anywhere else:

```fsharp
let v =
xs =
{
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}

// ✅ fields at 12, closing brace at 8
```

That one is mild, because the tokens in front of it are short. What the anchoring actually costs
shows when the token being anchored to is wider. These two differ in nothing but the width of the
operator:

```fsharp
let v =
xs
=== match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
// ^ column 8, which is 4 + 3 + 1: on the grid, by luck
```

```fsharp
let v =
xs
========= match yyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbb -> 2
// ^ column 14, which is 4 + 9 + 1: off the grid
```

Nothing chose 14. It is the sum of whatever happened to come before it, and it changes whenever any
of that changes. The three-character version landing on the grid is a coincidence, not a property.

It is worth asking why the arms are at 14 rather than at 12 or 16, since a formatter could put them
anywhere. Try 12, or 8, or 4:

```fsharp
let v =
xs
========= match yyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbb -> 2

// (4,9): error FS0058: Unexpected syntax or possible incorrect indentation: this
// token is offside of context started at position (3:15). Try indenting this further.
```

None of them compiles. A `match` opens a context at the column of its own keyword, and its clauses
have to sit at or past that column. So the low grid columns are not available: the operator's width
sets a floor under everything inside the right-hand side.

Column 16 does compile, and it is on the grid:

```fsharp
let v =
xs
========= match yyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbb -> 2
// ^ column 16, on the grid, but only the first multiple of 4 above 14
```

Widen the operator and the floor rises, and the grid column rises with it. Once a construct starts
part way through a line, no choice of column escapes what came before it: the best available is to
round up to the grid, and which line of the grid that is was still decided by the operator.

Those two examples are not among the five, so their operator starts a line rather than ending the
previous one, but the anchoring is the same mechanism either way. The five this note is about are
all one or two characters wide, which is why anchoring to them looks harmless. It is harmless by
accident, and an accident is a poor thing to build a rule on.

Off-grid columns are also how the third requirement gets broken in the first place, so the two are
related, but they are worth counting separately: a layout can satisfy the third and still be riddled
with them.

The way out is not to pick a better column. It is not to start the construct part way through a
line.

### And one thing to allow for: comments

A comment between the operator and the right-hand side forces a line break whether the layout wants
one or not, and the line it lands on has to sit past the left-hand side or it is no longer part of
the expression:

```fsharp
5 =
// a comment
5
```

## What was tried and does not work

Three layouts keep the right-hand side beside the operator. All three fail the third and fourth
requirements, and two of them fail something else first.

**Beside the operator, with no indent of its own** does not survive a comment. Given a list with a
comment in front of it, which forces the break:

```fsharp
let v =
xs =
// a comment
[
"aaaaaaaaaa"
"bbbbbbbbbb"
"cccccccccc"
"dddddddddd"
"eeeeeeeeee"
"ffffffffff"
]

// (3,5): error FS3156: Unexpected token '=' or incomplete expression
```

The comment and the list land at the column of the left-hand side, which is the one column nothing
may occupy. This is [#2944](https://github.com/fsprojects/fantomas/issues/2944).

**Beside the operator, indented one level** fixes that and puts brackets somewhere nobody would
choose. The same list, once as a binding and once as a right-hand side:

```fsharp
let a =
[
"aaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbb"
"cccccccccccccccccc"
"dddddddddddddddddd"
]
// ^ the `[` and the `]`, both at 4
// ^ the items at 8, one level inside them
```

```fsharp
let v =
xs = [
"aaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbb"
"cccccccccccccccccc"
"dddddddddddddddddd"
]
// ^ ⛔ the `]` at 8
// ^ ⛔ the `[` at 9, one column right of its own closing bracket
// ^ ⛔ the items at 12, three columns inside the `[` rather than four
```

Nothing about the list changed. What changed is what sits to its left.

**Beside the operator, indented unless the right-hand side opens a bracket** fixes that in turn, by
asking what the right-hand side is. It needs a list of constructs that has to be kept in step with
how each of them indents itself, and it still fails the third requirement, because the constructs
that follow the name on the left are not brackets. The `match` in the third requirement above was
formatted by this layout.

All three fail the same way on the last two requirements: seventeen of the forty examples move when
the name on the left changes by a single character, and seventy-three lines land on columns that are
not a multiple of the indent size.

## The layouts worth considering

Nothing may sit at the column of the left-hand side, so there are two places the operator can go:
at the end of the left-hand line, or one level in on the next one. Each of those leaves a choice
about the right-hand side.

### The right-hand side on a line of its own, when it does not fit

The operator stays at the end of the left-hand line. Nothing follows it unless all of the right-hand
side fits there; otherwise the right-hand side moves down one level and lays itself out from that
column, as the body of a `let` binding would.

```fsharp
let v =
xs =
[ "aaaaaaaaaa"; "bbbbbbbbbb"; "cccccccccc"; "dddddddddd"; "eeeeeeeeee" ]
```

```fsharp
let v =
xs =
{
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}
```

```fsharp
let v =
xs =
seq {
aaaaaaaaaaaaaaa
bbbbbbbbbbbbbbb
ccccccccccccccc
ddddddddddddd
}
```

```fsharp
let v =
xs =
match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
```

```fsharp
let v =
xs =
try
aaaaaaaaaaaaaaaaaaaaaaaaaaaa ()
with ex ->
bbbbbbbbbbbbbbbbbbbb ()
```

```fsharp
let v =
xs.ReplaceEverything(11111, 22222).ReplaceEverything(33333, 44444).TrimEnd() =
expected
.ReplaceEverything(55555, 66666)
.ReplaceEverything(77777, 88888)
.TrimEnd()
```

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc = 0
```

Every right-hand side starts at the same column whatever it is, and that column is a multiple of
the indent size. Nothing can be indented twice, nothing can be read as a continuation of the
left-hand side, nothing moves when a name changes, and two lines out of two hundred and forty-six
sit off the grid, both of them a tuple placing its second element under its own opening parenthesis,
which it does under every layout. The `0` stays beside the operator because it fits.

Two things follow. The list comes back onto one line, because a right-hand side with a line of its
own has the whole width to fit into rather than whatever the operator left over. And the `0` only
stays put because of the words "when it does not fit", which is the one exception this layout has.

### The right-hand side on a line of its own, always

The same without the exception. Thirty-eight of the forty examples are identical to the layout
above. The two that differ are both cases where the right-hand side had room beside the operator and
is moved down anyway:

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc =
0
```

```fsharp
let v =
xs
.ReplaceEverythingEverywhere(11111, 22222)
.ReplaceEverythingEverywhere(33333, 44444)
.TrimEnd() =
[ "aaaaaaaaaa"; "bbbbbbbbbb"; "cccccccccc"; "dddddddddd" ]
```

It is worth asking what the exception buys, because it is not free. "The right-hand side always
starts a line of its own" can be predicted from the code alone. "Unless it fits beside the operator"
needs the page width and a column count. Against that, six tests in Fantomas exist because someone
reported a short right-hand side moving down, so the exception is not invented.

### The operator on the next line, right-hand side beside it

The operator moves down instead, one level in, and the right-hand side follows it on the same line.

```fsharp
let v =
xs
= [
"aaaaaaaaaa"
"bbbbbbbbbb"
"cccccccccc"
"dddddddddd"
"eeeeeeeeee"
]
// ^ ⚠️ the `]` is here, at 8
// ^ ⚠️ the `[` is here, at 10, two columns to the right of its own closing bracket
```

```fsharp
let v =
xs
= {
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}

// ⚠️ fields at 14 and the closing brace at 10, neither a multiple of the indent size
```

```fsharp
let v =
xs
= seq {
aaaaaaaaaaaaaaa
bbbbbbbbbbbbbbb
ccccccccccccccc
ddddddddddddd
}
```

```fsharp
let v =
xs
= match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2

// ⚠️ the clauses are at column 10, which is not a multiple of the indent size
```

```fsharp
let v =
xs
= try
aaaaaaaaaaaaaaaaaaaaaaaaaaaa ()
with ex ->
bbbbbbbbbbbbbbbbbbbb ()

// ⚠️ `with` at 10 and its body at 14, neither a multiple of the indent size
```

```fsharp
let v =
xs.ReplaceEverything(11111, 22222).ReplaceEverything(33333, 44444).TrimEnd()
= expected
.ReplaceEverything(55555, 66666)
.ReplaceEverything(77777, 88888)
.TrimEnd()
```

```fsharp
let v =
xs aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccc
= 0
```

It also decides the bracket question for everyone, and decides it badly. Hugging the bracket to the
operator is the Stroustrup shape, and this layout imposes it whatever bracket style is configured,
including on the people who chose `aligned` in order not to have it.

Worse, it is not actually Stroustrup. Under Stroustrup a closing bracket returns to the column the
construct started at, and the contents sit one level inside that:

```fsharp
let a = [
"aaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbb"
"cccccccccccccccccc"
"dddddddddddddddddd"
]
```

Here the `[` is at column 10, the items at 12 and the `]` at 8. The items are two columns from their
own opening bracket rather than four, and the closing bracket is two columns to the left of the
opening one, lining up with neither it nor anything else. It is the Stroustrup shape with the
alignment that makes Stroustrup work taken out of it.

It meets the first three requirements and fails the fourth completely. The right-hand side is
placed relative to the operator, so its column is the operator's column plus the operator's width
plus a space, which is never a multiple of the indent size. Across the forty examples it puts 73
lines off the grid, the same as the layouts already rejected, against 2 for the layout above.

The record shows it: fields at 14, closing brace at 10.

```fsharp
let v =
xs
= {
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}
```

And because the column is the operator's width, it changes with the operator. Same expression, `=`
against `%%`:

```fsharp
let v =
xs
= match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
// ^ ⚠️ the clause bars at 10, which is 8 for the operator + 1 for `=` + 1 for the space
```

```fsharp
let v =
xs
%% match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
// ^ ⚠️ the clause bars at 11 now: 8 + 2 for `%%` + 1
```

`=` and `%%` differ by one character, so the damage is bounded here. It is bounded only because the
set is `=`, `>`, `<`, `%` and `%%` and none of them is wider than two characters. Nothing about the
layout makes it so.

### The operator on the next line, right-hand side below it

Putting the right-hand side on the line after the operator removes that anchor.

```fsharp
let v =
xs
=
[ "aaaaaaaaaa"; "bbbbbbbbbb"; "cccccccccc"; "dddddddddd"; "eeeeeeeeee" ]
```

```fsharp
let v =
xs
=
{
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}
```

```fsharp
let v =
xs
=
seq {
aaaaaaaaaaaaaaa
bbbbbbbbbbbbbbb
ccccccccccccccc
ddddddddddddd
}
```

```fsharp
let v =
xs
=
match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
```

```fsharp
let v =
xs
=
try
aaaaaaaaaaaaaaaaaaaaaaaaaaaa ()
with ex ->
bbbbbbbbbbbbbbbbbbbb ()
```

```fsharp
let v =
xs.ReplaceEverything(11111, 22222).ReplaceEverything(33333, 44444).TrimEnd()
=
expected
.ReplaceEverything(55555, 66666)
.ReplaceEverything(77777, 88888)
.TrimEnd()
```

```fsharp
let v =
xs aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccc
=
0
```

Nothing is placed relative to anything. The same `=` and `%%` comparison comes out identically, and
the off-grid count drops from 73 to the same 2 as the layout that leaves the operator where it is.
It meets all four requirements, which makes it the only real alternative to the two above.

What it costs is an operator alone on a line in every case, which no F# code looks like today, and
thirty-five more lines than putting the right-hand side down instead. It buys nothing the two above
do not already have, so the proposal is built from them. A further variant puts the right-hand side
one level deeper again; it reads no better and costs another twenty-four lines.

## What is proposed, and what I need blessed

**The rule.** Read the expression as `lhs operator rhs`, where `operator` is one of `=`, `>`, `<`,
`%` and `%%`. Working down from the easiest case:

```
1. it all fits on one line

lhs operator rhs

2. it does not fit, so lhs breaks over several lines, but rhs is short enough
to stay where it is

lhs
.spread()
.overSeveralLines() operator rhs

3. rhs does not fit after the operator, so it goes down one level

lhs operator
rhs

4. lhs is multiline as well, so the operator goes down too, between the two

lhs
.spread()
.overSeveralLines()
operator
rhs

5. except under fsharp_multiline_bracket_style = stroustrup, where an rhs that
opens a bracket keeps hugging the operator, in cases 3 and 4 alike

lhs operator [
rhs
]
```

A yes to that is enough for me to open a pull request against the style guide and implement it in
Fantomas. Given these:

```fsharp
let v = xs = { XXXX = 1; YYYY = 2; ZZZZ = 3; WWWW = 4; VVVV = 5; TTTT = "sevenseven" }
let v = xs = match yyyyyyyyyyyyyyyyy with | Aaaaaaaaaaaaaaaaaaaa -> 1 | Bbbbbbbbbb -> 2
let v = xs = try aaaaaaaaaaaaaaaaaaaaaaaaaaaa () with ex -> bbbbbbbbbbbbbbbbbbbb ()
let v = someFunction aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccc = 0
```

it gives:

```fsharp
let v =
xs =
{
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}
```

```fsharp
let v =
xs =
match yyyyyyyyyyyyyyyyy with
| Aaaaaaaaaaaaaaaaaaaa -> 1
| Bbbbbbbbbb -> 2
```

```fsharp
let v =
xs =
try
aaaaaaaaaaaaaaaaaaaaaaaaaaaa ()
with ex ->
bbbbbbbbbbbbbbbbbbbb ()
```

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc = 0
```

The last keeps its `0` beside the operator, because it fits there.

### The five smaller choices

Each is a pair: what I would do, and what I would do instead. I have an answer to all of them and
would take it if nobody says otherwise, so a reply of "yes to all" is a complete answer.

**Does a short right-hand side stay beside the operator?**

I do not feel strongly about this one. Keeping the exception spends no line on `0`; dropping it buys
a rule that can be predicted from the code alone. What the exception costs is set out under the
shortcomings below.

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc = 0

// I would do this
```

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc =
0

// rather than this, which spends a line on `0` but needs no exception to state
```

**Does the operator take a line of its own when both sides are multiline?**

This is where the rule is weakest without it: the last line of the left-hand side and the first line
of the right-hand side land on the same column, so the whole boundary is carried by a trailing `=`
and by `expected` not beginning with a dot. It fires on one of the forty examples and costs one
line; a record, a `match` and a `try` are untouched, because their left-hand side is one line.

```fsharp
let v =
xs
.ReplaceEverythingEverywhere(11111, 22222)
.ReplaceEverythingEverywhere(33333, 44444)
.TrimEnd()
=
expected
.ReplaceEverythingEverywhere(55555, 66666)
.ReplaceEverythingEverywhere(77777, 88888)
.TrimEnd()

// I would do this
```

```fsharp
let v =
xs
.ReplaceEverythingEverywhere(11111, 22222)
.ReplaceEverythingEverywhere(33333, 44444)
.TrimEnd() =
expected
.ReplaceEverythingEverywhere(55555, 66666)
.ReplaceEverythingEverywhere(77777, 88888)
.TrimEnd()

// rather than this, where `.TrimEnd() =` and `expected` share a column
```

**Under the Stroustrup bracket style, does a bracket keep hugging the operator?**

Someone who set `fsharp_multiline_bracket_style = stroustrup` did so because they want a brace to
hug what precedes it, and the rule as stated takes that away from a comparison while leaving it in
place for a binding. It costs the uniform starting column, and only for the people who asked for
brackets to hug. Everything that is not a bracket is unaffected, so the `match`, the `if` and the
`try` still stop following the name on the left.

```fsharp
let v =
xs = [
"aaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbb"
"cccccccccccccccccc"
"dddddddddddddddddd"
]

// I would do this, for people who set fsharp_multiline_bracket_style = stroustrup
```

```fsharp
let v =
xs =
[
"aaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbb"
"cccccccccccccccccc"
"dddddddddddddddddd"
]

// rather than this, which is what everyone else gets and ignores the setting
```

**When that hug and a multiline left-hand side meet, which one gives way?**

The setting was asked for explicitly and the operator rule was not, so the hug wins. It is not free:
the items land at column 8 where the two calls on `xs` already are, so six consecutive lines share a
column with two of them belonging to the left operand and four to the right. Anyone who would rather
have the operator take its line here gets that by not setting `stroustrup`, which is the same choice
they already make everywhere else in their code.

```fsharp
let long =
xs
.ReplaceEverythingEverywhere(11111, 22222)
.ReplaceEverythingEverywhere(33333, 44444)
.TrimEnd() = [
"aaaaaaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccc"
"dddddddddddddddddddddd"
]

// I would do this: the setting was asked for, so it is honoured
```

```fsharp
let long =
xs
.ReplaceEverythingEverywhere(11111, 22222)
.ReplaceEverythingEverywhere(33333, 44444)
.TrimEnd()
=
[
"aaaaaaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccc"
"dddddddddddddddddddddd"
]

// rather than this, which reads better here but overrules the setting
```

**Does the rule apply to all five operators, or only to `=`?**

Every problem the rule solves is present for all five. A `>` with a record on its right places the
fields from the brace column and moves them when the name on the left changes, exactly as `=` does.
What it costs is a line ending in `>`, which is unusual only because it does not happen today, not
because it is unclear: the operator cannot go to the column of the left-hand side, so nothing else
could follow it. The alternative is two rules, one for `=` and one for the rest, paid for on every
read.

```fsharp
let v =
xs >
{
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}

// I would do this
```

```fsharp
let v =
xs > {
XXXX = 1
YYYY = 2
ZZZZ = 3
WWWW = 4
VVVV = 5
TTTT = "sevenseven"
}

// rather than leave `>`, `<`, `%` and `%%` with this and have two rules to remember
```

### Where the proposal falls short

The exception costs predictability. "When it does not fit" cannot be evaluated by reading the code;
it needs the page width and a column count. These two inputs differ only in the length of the last
argument:

```fsharp
let v = xs aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccc = 0
let v = xs aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccc = 0
```

and come out in two different shapes:

```fsharp
let v =
xs
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccc = 0
```

```fsharp
let v =
xs aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccc =
0
```

In the first the application breaks across three lines and the `0` stays beside the operator. In the
second the application fits on one line, which leaves no room for the `0`, so it goes down instead.
Shortening the code made the output taller and changed its shape, and nothing on the page says which
of the two you are going to get.

A rule with no exception, where the right-hand side always takes a line of its own, does not have
this. That is what it buys, and the cost is the line spent on `0`.

**One thing I am not asking about here**, because it is bigger than this note. `=`, `>` and `<` sit
in this set because of a decision taken in 2013 to stop a break landing inside the right-hand
application, and formatters break applications properly now. `%` and `%%` have to stay where they
are, for the reason in the first section, but the other three do not obviously have to. If the right
answer is that `=` should not sit at the end of the left-hand line at all, that is worth knowing,
and it would replace this proposal rather than amend it.

Whatever is decided, the guides currently say one sentence about multiline infix expressions:

> Separate binary operators by spaces. Infix expressions are OK to lineup on same column

and that sentence cannot be written for any of these five operators, because the column it asks for
is the one column they may not occupy. So there is nothing to fall back on. Whichever answer wins,
the gap in the guides is worth closing, and closing it means someone has to choose.

## How this was measured

I built every layout above on a branch and ran them against forty examples:
[nojaf/fantomas@no-break-infix-layout](https://github.com/nojaf/fantomas/tree/no-break-infix-layout).
Each example is formatted twice, under two names differing by one character, and four things are
checked mechanically rather than by eye: the output parses, formatting it again returns the same
text, an infix `%` has not become a splice, and the rename moved nothing.

| layout | invalid | not idempotent | meaning changed | shifted by a rename | off-grid | lines |
| --- | --- | --- | --- | --- | --- | --- |
| beside, no indent | 1 | ok | ok | 17 | 74 | 238 |
| beside, indented | ok | ok | ok | 17 | 73 | 240 |
| beside, bracket-aware | ok | ok | ok | 17 | 73 | 240 |
| right-hand side on its own line when needed | ok | ok | ok | ok | 2 | 246 |
| right-hand side on its own line always | ok | ok | ok | ok | 2 | 248 |
| operator next line, right-hand side beside | ok | ok | ok | ok | 73 | 273 |
| operator next line, right-hand side below | ok | ok | ok | ok | 2 | 281 |
| operator next line, right-hand side below and deeper | ok | ok | ok | ok | 2 | 305 |
| the proposal, with both refinements | ok | ok | ok | ok | 2 | 247 |

`off-grid` counts lines whose indentation is not a multiple of the indent size. `lines` is the total
line count, as a measure of verbosity. The last row is the proposal.

The branch's README explains what the numbers mean, why the two names differ by exactly one
character, what the table does not cover, and how to run any of it, including over your own code.

---

Thank you for reading all of this. Much appreciated, ✌️ Florian

Contributor guide

No contributing guide indexed for this repository

Research direction

Read the official F# style guide linked in the issue and the related #2944 discussion first. Compare the proposed infix-expression layouts against the stated parsing, indentation, line-length, and comment requirements, using the supplied examples as the starting point. Done means the community has reached and documented a decision on the formatting rule.

Written by the indexing model from the issue text.

Assessment

Tech stack
fsharp
Domain
compilers, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.