[Poll] Consider changing the default of fsharp_multiline_bracket_style
Hello everyone,
I recently had a conversation about how Fantomas formats records by default (fsharp_multiline_bracket_style = cramped).
There’s a configuration setting for this: fsharp_multiline_bracket_style.
The benefit of the cramped style is that it’s compact and may feel familiar to developers coming from OCaml or Haskell.
let myRecord =
{ Level = 1
Progress = "foo"
Bar = "bar"
Street = "Bakerstreet"
Number = 42 }
type Range =
{ From: float
To: float
FileName: string }
let a =
[| (1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10, 11, 12)
(13, 14, 15)
(16, 17, 18)
(19, 20, 21) |]
The main drawback of this style is that it’s harder to manipulate record fields during copy/paste operations.
Indentation can also feel slightly off:
// indent_size = 4
let myRecord =
// Start of { at 4 spaces
{ Level = 1
// Start of record field at 6 spaces
Progress = "foo"
This breaks what I call the “indentation flow.”
This topic deserves its own discussion, but it’s not the main focus here.
The other available styles (aligned and stroustrup) do not have this issue:
// aligned
let myRecord =
{
Level = 1
Progress = "foo"
Bar = "bar"
Street = "Bakerstreet"
Number = 42
}
type Range =
{
From: float
To: float
FileName: string
}
let a =
[|
(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10, 11, 12)
(13, 14, 15)
(16, 17, 18)
(19, 20, 21)
|]
// stroustrup
let myRecord = {
Level = 1
Progress = "foo"
Bar = "bar"
Street = "Bakerstreet"
Number = 42
}
type Range = {
From: float
To: float
FileName: string
}
let a = [|
(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10, 11, 12)
(13, 14, 15)
(16, 17, 18)
(19, 20, 21)
|]
These styles make it easier to add or remove record fields, and indentation stays consistent with multiples of the indent_size.
The F# style guide mentions all three of these styles.
So, the key question is:
Should we reconsider the default style?
We (the Fantomas core team) would like to get a sense of how the community feels about this.
We are not promising any changes, but if there is strong community consensus in favour of something else, we will consider it seriously.
This has been the default for many years, so we will not make any changes lightly.
That said, we’d love to hear your preference.
Please react to the first message in this issue with:
- 👀 if you prefer to keep the current default (cramped)
- 🎉 if you’d prefer aligned
- 🚀 if you’d prefer stroustrup
We’ll only count reactions to the first post in this issue.
Reactions or opinions elsewhere will not be included.
//cc @dawedawe @josh-degraw
Cramped has inconsistent indentation, sometimes 4, 3, or 2 spaces, so should be binned.
As a proponent of the One True Brace Style in C family languages I would prefer the Stroustrup option. It offers the same vertical space saving as compact vs. aligned, while avoiding the odd indentation.
I find stroustrop the nicest to work with.
Switching the order of things is great with every line then, it doesn't matter which comes first or last, I can swap lines without doing extra work to move the bracket.
It's also has the best trade off for indent vs number of lines I think.
In F# you can also add methods to a record type, which is also not standardized.
I'm writing it as:
type Range = {
From: float
To: float
FileName: string
} with
member this.FromToString() =
$"{this.From}-{this.To}"
I'm team "aligned" because I like the symmetry that helps my eyes parse code faster - even if the code is one line longer per record. As long as there is a setting for the style, I'm totally okay with a different default style.
I decided to choose stroupstrup, since it is the closest to the indendation based style that we are used from FSharp otherwise.
Its also the easiest to read, I think.
Just to add a bit from the tooling and language design perspective. F# is indentation sensitive, and there are many cases where allowing something to be less indented makes it less suitable for the editor tooling. In the last couple of years we've significantly improved quality of the parser recovery during typing and many of these improvements assume that inner parts of expressions/statements are always more indented.
Stroustrup looks great and has many advantages, but out of these three styles it works the worst with the F# parser, because it was invented with a different language family in mind.
From the current vote count (24-aligned, 84-stroustrup, 10-keepCurrentDefault) seems clear to me that the default should change. However, I'd argue that @auduchinok's input here is important, so maybe stroustrup should not be the default to switch to? (I'm biased, I'm team-aligned)
From the current vote count
We need more input before deciding. So far the votes suggest a different option is preferred as the default, but that is relative: Fantomas averages about 500 downloads per day according to the NuGet stats. The NuGet numbers don't show the full picture of all users. It's hard to reach everyone, which is why we want to hear enough voices before making a decision.
Just to add a bit from the tooling and language design perspective. F# is indentation sensitive, and there are many cases where allowing something to be less indented makes it less suitable for the editor tooling. In the last couple of years we've significantly improved quality of the parser recovery during typing and many of these improvements assume that inner parts of expressions/statements are always more indented.
Stroustrup looks great and has many advantages, but out of these three styles it works the worst with the F# parser, because it was invented with a different language family in mind.
The parser has to support all three options anyway. This is just about the Fantomas default.
The parser has to support all three options anyway. This is just about the Fantomas default.
"Has to" is a bit of an assumption. The parser mostly supports three styles until it doesn't. You can probably write some extreme Stroustrup-style code that will hit parser limitations.
Overall, and I don't mean to be negative, F# wasn't designed with a formatter in mind. The parser accepts certain constructs, but code styling was never part of its original design, so these behaviors are more accidental.
Eugene's remark is valuable input. If we make Stroustrup the default, I expect more issues from people exploring uncharted territory where parser limitations may come into play.
"Has to" is a bit of an assumption. The parser mostly supports three styles until it doesn't.
As far as I know, Stroustrup style has been valid F# since the "lightweight syntax" was introduced and was always part of the spec (§15.1.4). I have a hard time imagining the parser will stop to support it.
I'm no lore expert like yourself on this, but I don't quite distill that stroustrup has always been a thing from reading https://fsharp.github.io/fslang-spec/lexical-filtering/#1514-offside-lines
I'm happy to be wrong there though.
It says "[an offside line is introduced at ...] The column of the first token of a (, { or begin token." So, it does not matter if that first token is on the same line as the brace (like in cramped style and Stroustrup) or on a different one (like in aligned) and also not if the brace is on a separate line or not. This rule was extended by the RFCs FS-1054 and FS-1108 to other constructs like opening parentheses and brackets. The undentation of the closing brace is specified for types in §15.1.9. I am not sure if there was ever an RFC that extended this to other constructs, but it is implicit in FS-1054 and certainly has been supported by the compiler for a very long time.
I prefer aligned, but am perfectly ok with stroustrup. As long as it is not cramped :-). Thanks @nojaf for setting this poll up.
Stroustrup looks great and has many advantages, but out of these three styles it works the worst with the F# parser, because it was invented with a different language family in mind.
:+1: I felt like this deserves an example.
I like Stroustrup (because it saves some indentation), but I also like Aligned. Here’s a concrete example of where Stroustrup can make code harder to edit, due to parsing rules.
[<Xunit.Fact>]
let ``Ticket should be escalated even after being reopened`` () =
let timeline = [
ticketCreated
statusAssigned
resolved
reopened
moreInfoRequested
escalated
]
// rest of test omitted
When making a change, you realize that you need to call wrapEvent on each thing in timeline. Easy peasy, right? Just slap a |> List.map wrapEvent on it.
[<Xunit.Fact>]
let ``Ticket should be escalated even after being reopened`` () =
let timeline = [
ticketCreated
statusAssigned
resolved
reopened
moreInfoRequested
escalated
]
+ |> List.map wrapEvent
Computer says no:
Test.fs(77,9): error FS0010: Unexpected infix operator in binding. Expected incomplete structured construct at or before this point or other token.
Test.fs(69,5): error FS3118: Incomplete value or function definition. If this is in an expression, the body of the expression must be indented to the same column as the 'let' keyword.
Test.fs(69,5): error FS0588: The block following this 'let' is unfinished. Every code block is an expression and must have a result. 'let' cannot be the final code element in a block. Consider giving this block an explicit result.
Test.fs(102,1): error FS0010: Incomplete structured construct at or before this point in implementation file
You first have to edit the code to Aligned style, then add the pipe.
[<Xunit.Fact>]
let ``Ticket should be escalated even after being reopened`` () =
let timeline =
[
ticketCreated
statusAssigned
resolved
reopened
moreInfoRequested
escalated
]
|> List.map wrapEvent
(You could also argue that I should stick List.map wrapEvent at the front (no pipe), but imagine I need to pipe twice.)
@lydell thanks a bunch for this example! It is pretty wild that
let timeline = [
ticketCreated
statusAssigned
resolved
reopened
moreInfoRequested
escalated
] |> List.map wrapEvent
is invalid code.
Existing Fantomas users: it would be great if you could use
[*.{fs,fsx}]
fsharp_multiline_bracket_style = stroustrup
on your codebase and report and problems we didn't catch!
That's weird indeed, and in my view a compiler bug. It does speak against making Stroustrup the default at this time. I still hope that Stroustrup stays as an option though.
I've voted for stroustrup, but then also for aligned, as I'd take anything over cramped which doesn't even align to a multiple of the indent size.
The benefit of the cramped style is that it’s compact and may feel familiar to developers coming from OCaml or Haskell.
Maybe older OCaml codebases, but recent ocamlformat formats record type declarations as stroustrup and list/record values/arguments as aligned by default.
Existing Fantomas users: it would be great if you could use [stroustrup] on your codebase and report and problems we didn't catch!
I reformatted our codebase (around 200000 lines of F#) with stroustrup and found this error:
let lostMessages = [
]
Doesn't compile because the closing ] has to be indented.
Before the cod looked like (style aligned):
let lostMessages =
[
]
Otherwise, no problems, but I still don't like it 😆
Maybe older OCaml codebases, but recent ocamlformat formats record type declarations as stroustrup and list/record values/arguments as aligned by default.
How do we feel about such a combination?
How do we feel about such a combination?
A new mode is not something we are considering here.
You can open a discuss on https://github.com/fsprojects/fantomas/discussions if you like.
Not a fan of cramped at all. So, anything that isn't that would be nice. Though I think I like stroustrup the most of the alternatives. Cramped just looks weird to me.
@auduchinok, does Stroustrup have negative effects compared to aligned on the dev experience inside the IDE? Like worse error messages due to parser recovery problems? Especially, for non-complete types, expressions etc. while coding?
I've experimented with Fantomas from time to time but have never managed to cross the chasm. So I should keep out of this, but anyway...
While I get the appeal of never debating formatting ever again, none of the above configs truly make me happy:-
- cramped would make more sense if its default and/or the OP showed it with indents in multiples of 4 as common in most docs. It also means you don't trigger whitespace changes when switching things from anonymous vs named records and/or lists to arrays (i.e. when
[or{|becomes[|or{). EDIT: Having looked deeper, I can appreciate it's a faithful rendition of a documented style, so tweaking it or talking about that is way off topic - re Stroustrup, one thing that's been touched on but not called out explicitly as a quality of life thing overall is that it triggers the need for sprinkling of
withand/or fighting with indentation rules in a way that neither aligned or cramped do, that's something that I'd prefer an F# beginner not to be subjected to. I suspect people coming from typical Stroustrup formatted languages find that very frustrating as it superficially makes it look just like the structure/ tolerance they wanted, only to land them in a world of pain until they read the docs/messages and/or figure out some invariably incomplete heuristic - re aligned, for C# I don't do redundant braces, so being forced to waste vertical space/draw inordinate emphasis to things by giving them lots of space grates
If you hold a gun to my head to pick a least bad default, that's aligned. But I don't think aligned strikes a useful balance between truly pleasing some and frustrating others.
So I voted for cramped/not changing the default as there is no clear winner.
IME the fact that the above inconsistencies make none of the three easy to love hinder adoption in closed source codebase.
The closest to what I do IRL is cramped, (but I do it with indents of 4 as mentioned above, which is a hybrid aligned-cramped, which is definitely not common and/or perfect)
Perhaps I should put this as a discussion elsewhere as it's some distance from the OP topic, but it does seem that the number of tradeoffs/imperfections in play makes the voting more complex than it seems on the surface?
I like stroustrup best aesthetically, voted aligned as it feels like the safest default for the reasons already mentioned. Not in to cramped.
@auduchinok, does Stroustrup have negative effects compared to aligned on the dev experience inside the IDE?
Yes, exactly like the cases above where the parsing breaks, and I suspect there may be more. Almost all IDE features rely on reasonably good parsing.
We (the Fantomas core team) would like to get a sense of how the community feels about this.
We need more input before deciding. So far the votes suggest a different option is preferred as the default, but that is relative: Fantomas averages about 500 downloads per day according to the NuGet stats. The NuGet numbers don't show the full picture of all users. It's hard to reach everyone, which is why we want to hear enough voices before making a decision.
@nojaf How much does the language-adoption perspective play into this decision?
The result of this poll may represent how the existing F# community feels about the indentation style.
But since it is just the default value you are considering changing, would the not-yet-F# developers not be a better target for the poll? At least seen from the language-adoption perspective.
I.e. ask 200 random TypeScript developers what style they would find the easiest to transition into.
My guess would be stroustrup :)
If changing the default value to stroustrup could remove even a tiny bump on the F# adoption path, then I think that argument alone is enough reason to do it.
Some consistency enthusiasts may still switch to aligned, and newcomers from OCaml/Haskell may choose cramped for the familiarity.
I love how we've adopted a measure introduced solely to save space in a printed book - where extra space == additional cost - as the ideal standard 😢 Its not like we print listings to fix errors/debug/review any more either (I have two copies of the book and have scribbled on a lot of printed listings)
Whitespace is more or less free - having matching pairs of things aligned is helpful to the bits of the brain that do pattern matching. 🦕👈me
Its not like we print listings to fix errors/debug/review any more either (I have two copies of the book and have scribbled on a lot of printed listings
Speak for yourself, I hook up all my applications up to whatever printer is available on the network to automatically print out the error, stack trace and if possible relevant code. I make the compiler print out errors like this as well.
This way, everything is nicely documented for future reference.