Repository navigation
[style-guide] Extra space in fluent code #648
Description
Activity
@dsyme These lowercase method names look very non-idiomatic to me.
The current approach is to add space for lowercase-starting names, since it's the style normally used for curried functions, and not to add space before uppercase-starting names that normally are references to methods. Unfortunately, there's no way to distinguish functions and method invocations based on syntax, and I think it's a very sensible approach with these constraints.
This is a tricky one. We currently have some settings to control whether there should be a space or not.
fsharp_space_before_lowercase_invocation
fsharp_space_before_uppercase_invocationSo using the default settings, if the function names are uppercase they don't have the space.
ExampleIf we were to change the current behaviour, it would require input from the style guide as the next person will lovely open an issue that there has been a regression. It might be a good idea to put out some rules when to have a space and when not.
For example, in this case, does the lambda remove the need for a space?
Or would a plain unit also require no spacexs.map(fun a -> a + 1).filter(fun a -> a > 1).dump ()?Reacted by Eugene AuduchinokThe current approach is to add space for lowercase-starting names, since it's the style normally used for curried functions, and not to add space before uppercase-starting names that normally are references to methods. Unfortunately, there's no way to distinguish functions and method invocations based on syntax, and I think it's a very sensible approach with these constraints.
I understand this perspective. That said, in the context of projects like DiffSharp and TorchSharp, lower case fluent is becoming more common (for good reasons - they are mimicking a Python API and it's the right choice in balance). FSharp.Core.Fluent also allows this naming though that's a slightly separate matter.
For example, in this case, does the lambda remove the need for a space?
Or would a plain unit also require no spacexs.map(fun a -> a + 1).filter(fun a -> a > 1).dump ()?I would imagine the rule would be "a chain of fluent of length two or more means no space on application throughout" so
xs.whack(a).mole(b)rather thanxs.whack(a).mole (b).I notice that in the default settings for uppercase the presence of parens in the original source is also relevant
I guess chained lower case application (not just
List.map(f)but an actual chainxs.map(f).map(g)) is just extremely rare, and that when it it present it represents fluent notation, so removing that last space will be right.There's no rush with this, I just noticed it when experimenting with fluent notations.
@dsyme These lowercase method names look very non-idiomatic to me.
I get that. The problem is they look really idomatic once you're in the F# world of DiffSharp and TorchSharp (or if coming fresh from Python, Javascript or Scala). And then you come back to .NET-focused programming and think about doing it. Leaky I know.
I get that. The problem is they look really idomatic once you're in the F# world of DiffSharp and TorchSharp (or if coming fresh from Python, Javascript or Scala).
Wouldn't it be better to have idiomatic .NET/F# wrappers for these libraries instead of trying to apply a not very compatible Python code style to F#, a different and a mature language, only making experience of using F# inconsistent (thus, harder to learn)?
I notice that in the default settings for uppercase the presence of parenthesis in the original source is also relevant
Well, no parenthesis leaves us with no other options than having the space anyway right?
xs.Prop.Map b // xs.Prop.Mapb is no longer a function call xs.Prop.Map(b)
or are you referring to something else?
I would imagine the rule would be "a chain of fluent of length two or more means no space on application throughout" so xs.whack(a).mole(b) rather than xs.whack(a).mole (b).
That could work out although we should probably come up with sufficient examples as the scope will be larger than that.
xs.[x].whack (a) // space or not? // what happens with long constructs? xs .whack( // a) .mole( // long expression b)
The current support now isn't always perfect, check out DotIndexedGetTests.fs and DotIndexedGetTests.fs.
All I'm trying to say is that there might be a bit more than meets the eye here. I'm open to change as long as we consider all things.Sorry to chime in, but I still don't get why the style guide (and hence, the fantomas defaults) recommends different spacing depending if the function is lowercase or not. (Disclaimer: I'm myself not a fan either of the decision of having F# conventions use lowercase for functions, given that it goes against .NET API style guidelines; and yes I know about the attribute that you can use to decorate your functions to become accessible with an uppercase...) I think the whole thing is very confusing, especially to newcomers (I will not put a "IMHO" here because my opinion is not that humble, I've mentored most of the developers of my company to learn F# so I know a thing or two about that).
Sorry to chime in, but I still don't get why the style guide (and hence, the fantomas defaults) recommends different spacing depending if the function is lowercase or not.
@knocte Historically, the space is usually added for curried F# functions applications, like in
M.f a borM.f (a, b) c, and no space is added for .NET methods calls, like inx.M(a)orx.M(a, (b, c)).The problem is functions and methods applications share the same syntax, and you can't use a single formatting rule for these two different styles without making half of the cases look bad. The current formatting rule was the best heuristic proposed for the problem: normally, functions are lowercase and methods are uppercase (and are not curried), so it works OK in the most of the cases for both functions and methods. The only cases where formatter produces unexpected results are when naming conventions are violated, like here with the lowercase methods.
@knocte Historically, the space is usually added for curried F# functions applications, like in M.f a b or M.f (a, b) c, and no space is added for .NET methods calls, like in x.M(a) or x.M(a, (b, c)).
From the perspective of an F# developer, the only difference between
x.foo bar bazandx.Foo (bar, baz)is that the latter is just receiving the arguments via a tuple. One cannot know if Foo is a C# method (.NET?) or an F# method that doesn't use currification.In fact, the best case in point is when using one only argument: as there is no tuple and no currification, then what should be used here, uppercase or lowercase? The standard/convention would just be much less confusing if an space was added always regardless if it's uppercase or lowercase.
I still don't get why the style guide (and hence, the fantomas defaults) recommends different spacing depending if the function is lowercase or not.
Sorry for being silent here, I guess I don't really have a good response for this remark. This is a historical thing I guess.
The standard/convention would just be much less confusing if an space was added always regardless if it's uppercase or lowercase.
Yes, for newcomers this would be a lot less confusing. But a lot of people will find
a.ToString ()very alienating I think. That being said, personally, if you give these things enough time, you eventually get over them. Feel free to raise this in the style guide, open an issue, perhaps it is worth discussing this again.Reacted by Andres G. AragonesesBut a lot of people will find a.ToString () very alienating I think.
It never felt alienating to me, that's why I enable all fantomas' space_before settings in my repos. But the reason might be that I'm biased because in my first .NET years when coding in C# I contributed to many repositories in the Mono ecosystem, and most of them shared some coding guidelines that advocated for adding a space to every invocation: https://www.mono-project.com/community/contributing/coding-guidelines/
perhaps it is worth discussing this again
Let's imagine we opened an issue and after some debate we agreed to change the standard to make it less confusing for newcomers (making the change in the style guide and so fantomas defaulting fsharp_space_before_uppercase_invocation to true), despite the alienating feeling expressed above: we would have solved only one piece of the cake, because then we would still have the inconsistency that fantomas could not add a space when doing fluent calls (and then we would still see the strangeness raised in this bug filed by Don).
So I just realised that, despite my dislike of it, the best thing to do here is actually do the opposite: remove the space when it's not needed. And bringing up
xs.Prop.Mapbnot being a function call misses the point here, because, obviously, there always needs to be a separation between the function and the argument, but that separation can be a parenthesis instead of a space, and it turns out that in F#, two braces together()is already the magical unit argument, but that doesn't mean that we need a separator for it, because it's a special argument which is not composed of alphanumeric characters so there is no need to treat it like every other possible argument.Therefore, the idea would be the following (let's see if I don't miss any of the possible cases):
- Invocation with no arguments (no space needed):
foo()orFoo() - Invocation with one argument (a space needed, unless parenthesis are already there):
foo barorfoo(bar)orFoo barorFoo(bar) - Currified invocation of two arguments (spaces needed, unless parenthesis already there):
foo bar bazorfoo(bar)(baz)orFoo bar bazorFoo(bar)(baz) - Non-currified invocation of two arguments, or invocation of one tuple-argument (parens are already there, therefore no space needed):
foo(bar, baz)orFoo(bar, baz) - (The most convoluted case: ) Currified invocation two arguments, both arguments being a tuple (despite spaces are not needed, we can just add them to make it more visually appealing and consistent with
foo bar baz):foo (bar1, bar2) (baz1, baz2)orFoo (bar1, bar2) (baz1, baz2)
If the coding style recommended the above style, there would be consistency across uppercase/lowercase invocations and across fluent calls (so, we could just make fantomas default to this, and converge the settings fsharp_space_before_uppercase_invocation and fsharp_space_before_lowercase_invocation into just a single one
fsharp_always_space_before_invocationwhich would default to false, and only switching it to true would mean seeing some inconsistencies).If it sounds too good to be true, let me know what did I miss, but if not, I can raise this in the style guideline as an issue or a PR.
- Invocation with no arguments (no space needed):
we can just add them to make it more visually appealing and consistent with
foo bar bazAnd when I wrote this I fell into my own trap, because then the last case above that I described as most convoluted case, wouldn't be able to be used in a fluent call:
Foo (bar1, bar2) (baz1, baz2).Foo (bar3, bar4) (baz3, baz4)wouldn't work AFAIU, so then we need to drop the spaces here too:Foo(bar1, bar2)(baz1, baz2).Foo(bar3, bar4)(baz3, baz4)just in case (even if the above would be very rare).7 remaining items
a not very compatible Python code style to F#
I'd say it is subjective (like my feedback), I think the idea of F# being it's own, in terms of feel of the language, irrespective of being firstly a dotnet language, doesn't make the style incompatible/not very compatible to me.
At some points, those conventions, that were set in framework design guidelines, they pertain to a very OO centric approach, with few distinctions, to make it look like "not a 1 to 1 of java", and I don't feel enforcing those same guidelines, in essence, is what is going to foster the ecosystem the most, for adoption, versus other concerns in the grand scheme.
Getting back to the OP, I will make a proposal to the style guide:
chained-lowercase-dot-notation-applications of length >= 2 do not add spacing on the final application
I think this is consistent - we either remove all the spaces (for N > 2) or none. The current situation of removing all but one of the spaces is just odd.
Reacted by Eugene AuduchinokIn Fabulous v2 we have a new DSL a la SwiftUI . the existing way causing some formatting issues . We added
fsharp_space_before_parameter = false fsharp_space_before_lowercase_invocation = falseto out .editorconfig to make it more consistent
(HStack(16.) { Label("New address") .textColor(Colors.StandardTextColor.Light, Colors.StandardTextColor.Dark) .alignStartHorizontal(true) .verticalTextAlignment(TextAlignment.Center) TextButton(AppResources.Cancel, CancelManualAddress, true) .alignEndHorizontal() }).gridRow(0)
Status update from the Fantomas side, four years on.
Short version: the space you flagged is the only one in a chain that was ever negotiable. Every other one is fixed by the parser.
Why
Fantomas 7 turned this into code that does not compile (fantomas#3364):
"yow" |> _.Substring(0, 16).ToLower() // input "yow" |> _.Substring(0, 16).ToLower () // output
Fixing it forced the question of what actually governs that space, and it splits in two:
- Intermediate calls are not a style question.
a.Foo (x).Bar()parses asa.Foo ((x).Bar()), a different program. Space, newline and comment are the same gap to the parser, so every call except the last is tight by grammar and no setting can override it. - The final call is the only free position. Nothing follows it to be reparsed.
Your original observation still holds exactly, on today's Fantomas:
xs.map (fun a -> a + 1) // one call, takes the space xs.map(fun a -> a + 1).filter (fun a -> a > 1) // two calls, only the last
"Removing all but one of the spaces is just odd" is fair. What I would say now is that the survivor is not arbitrary: it is the only position where a preference could ever apply.
The decided proposal is still unimplemented
chained-lowercase-dot-notation-applications of length >= 2 do not add spacing on the final application
We never did it, and it never reached the style guide. The guide's rules on this space are about casing and single applications, neither conditioned on chain length.
The redesign makes it cheap: length >= 2 is now readable straight off the tree. I have it working locally, and it moves almost nothing on the code I have to hand.
The one thing I would like an answer to
Does casing matter?
Your text says lower-case, and under default settings that is the same set as "wherever this is visible", since upper-case methods take no space anyway. They come apart when
fsharp_space_before_uppercase_invocationis on, as in the G-Research style, wherea.Foo(x).Bar (y)has the identical shape.- Lower-case only: near zero effect, upper-case styles keep today's behaviour.
- Any chain that already called: tidier, since the reason has nothing to do with the name's case, but it reflows upper-case fluent code.
My instinct is the second, because the reason is about the shape of the chain rather than the name. But that is the reading that changes people's files, and your text said lower-case, so I would rather ask than guess.
Where we are
Shipped in
8.0.0-alpha-014. The rules are written up at https://fsprojects.github.io/fantomas/docs/contributors/Chains.html, including the alternatives we turned down. Filed under Contributors on purpose: a proposal we are testing, not settled guidance. Where the guide does speak about chained expressions, Fantomas matches it.(@Smaug123 is this something the G-Research style guide would want to deviate on?)
- Intermediate calls are not a style question.
The redesign makes it cheap: length >= 2 is now readable straight off the tree. I have it working locally, and it moves almost nothing on the code I have to hand.
@nojaf Thank you for getting back on this!
Just to check you're asking for a 👍 to add this, making it the default in Fantomas? Or is it more "let's close this out and not fix this"?
I'm actually OK either way. It mattered most in the DiffSharp/TorchSharp context where F# code was head to head against Python code, and lowercase is everywhere as part of that, but realistically it's not where F# usage is at going forward.
@nojaf clarified:
Yes, this would be the new default and just wanted to double check that casing does or does not matter here. The only change I still need to make is to never have the space is length >= 2. Right now it still respects fsharp_space_before_uppercase_invocation and fsharp_space_before_lowercase_invocation
This is great, and gets my 👍
Reacted by Florian VerdonckRule: a call keeps the space before its parenthesis only when the whole thing being called is a
plain dotted name.Anything else in there takes the space away: a call, an index, a receiver that is not a name, or
a type application.It applies regardless of
space_before_uppercase_invocationand
space_before_lowercase_invocation. Those two only get a say where the rule allows the space at
all.The original report, on default settings:
xs.map(fun a -> a + 1).filter(fun a -> a > 1)
Keeps the space
Everything from the identifier to the call is dots and names.
Foo (x) // not a chain at all a.Foo (x) a.B.Foo (x) a.B.C.Foo (x) List.map (f) Fantomas.FCS.Text.Range.unionRanges (r1, r2)
"Keeps the space" means the existing settings decide, exactly as they do today. Fantomas has two
of them,space_before_uppercase_invocation(default false) and
space_before_lowercase_invocation(default true), and the part of the name immediately before
the invocation is what picks between them. Nothing earlier in the name has a say:a.B.foo (x) // lower-case `foo` decides, so this is spaced on default settings a.b.Foo(x) // upper-case `Foo` decides, so this is tight on default settings
So on default settings the list above is spaced only where the final name is lower-case. The
examples further down usespace_before_uppercase_invocation = trueso the space is visible in
the upper-case cases too.Deeply qualified module functions are deliberately untouched. Nothing about them is fluent, and
the dots in them are namespace depth rather than steps in a chain.Goes tight
Shown with
space_before_uppercase_invocation = true, which is what makes most of these visible
in the first place.A call in the path:
a.Foo(x).Bar(y) Foo().Bar(y) Foo<int>().Bar(y) Dictionary<string, int>().Add(k, v) x.Foo<int>().Bar<string>(y)
An index:
arr[0].Foo(x) arr.[0].Foo(x)
A receiver that is not a name:
(f x).Bar(y) "yow".Substring(0, 3) [ 1; 2 ].Contains(1) {| X = 1 |}.ToString()
A type application, on either the receiver or the call:
X<Y>.Foo(x) X<Y>.B.Foo(x) a.Foo<int>(x) a.B.Foo<int>(x) List.map<int>(f)
And a type application with no dots in front of it at all. This one is new, and it is worth
stating separately because there is no chain involved. A type application followed by
parentheses is tight on its own:unbox<int>(obj) f<int>(x) Foo<int>(x)
It only ever comes up because the parentheses are already there in the source. Fantomas does not
add parentheses, so the far more common form is left exactly as written and the rule never
reaches it:unbox<int> obj
Why this rather than counting the applications
The proposal earlier in this thread
was "chained-lowercase-dot-notation-applications of length >= 2 do not add spacing on the final
application". Two things argue against it.It leaves output mixed in a way you cannot see:
a.B.C.Foo (x) // spaced, only one call a.Foo(x).Bar(y) // tight, two calls
To tell those apart you have to scan the line for an earlier
(). Asking instead whether the
thing being called is a plain dotted name is one property you can read straight off the name.And counting applications does not reach this:
Foo().Bar (y)
One application, so the count leaves it alone, yet it has exactly the problem reported at the
top of this issue: a tightFoo()sitting beside a spaced.Bar (y)on one line.Confirming
@dsyme happy with this as the rule? Once it has a 👍 here I will implement it in Fantomas and
open a PR against the style guide.Yeah this is great, thank you @nojaf
- added a commit that references this issue
on Aug 27, 2026 - added a commit that references this issue
on Sep 15, 2026 - added 2 commits that reference this issue
on Sep 21, 2026
I notice that a sequence of fluent applications is getting an extra space in the last application, so for
we get
Note the space after the "filter".
I understand why this is happening, and that in some sense it's the consistent application of rules. However I think I'd expect
That is, two or more fluent applications all get no space. A single application would still get a space, per the usual rules, e.g.
gets
That said, it's a tricky business and stems from the fact that consecuitive fluent notation is only supported in F# by omitting spaces that are otherwise natural.
Issue created from fantomas-online
Code
Result
Problem description
Please describe here the Fantomas problem you encountered.
Check out our Contribution Guidelines.
Extra information
Options
Fantomas 4.6 branch at 10/05/2021 16:21:15 - 835872c91bf490f6aeffb1efc77f2b47f517aec0
Default Fantomas configuration
Did you know that you can ignore files when formatting from fantomas-tool or the FAKE targets by using a .fantomasignore file?