Skip to content

One-level ifs#479

Merged
brandonchinn178 merged 14 commits intofourmolu:mainfrom
cblp:one-level-ifs
Aug 10, 2025
Merged

One-level ifs#479
brandonchinn178 merged 14 commits intofourmolu:mainfrom
cblp:one-level-ifs

Conversation

@cblp
Copy link
Copy Markdown
Contributor

@cblp cblp commented Jul 12, 2025

Original issue tweag/ormolu#299

Closes #80

Based on @wldhx's work

Solved questions

1. Should we allow 2-line "if-then-break-else" layout?

if condition then true branch
else false branch

My personal opinion: No. Either one line or minimum 4.

-- one line
if condition then true branch else false branch

-- minimum 4
if condition then
    true branch
else
    false branch

2. What do we do with multiline conditions

-- Ormolu and default Fourmolu
if condition1
  || condition2
  then true branch
  else false branch

-- Fourmolu with current version of one-level-ifs
if condition1
  || condition2 then true branch
else false branch

-- My personal taste
if condition1
  || condition2
then true branch
else false branch

Unsolved questions

3. Option name

Maybe one-level-ifs is a bad name, but that was in the original patch. I'd prefer indent-then-else :: Bool.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jul 12, 2025

👋 @cblp
Thank you for raising your pull request.
Please make sure you have followed our contributing guidelines in DEVELOPER.md. We will review it as soon as possible!

Reviewer: Please verify the following things have been done, if applicable.

  • A file has been added to changelog.d/
  • Docs have been updated in web/site/pages/config/
  • Tests have been added
  • Diff files in compat-tests/ either:
    1. Have NOT been changed, or
    2. Modifies fourmolu.yaml with an appropriate option to keep the same formatting, or
    3. Modifies Haskell files with new formatting

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Jul 20, 2025

Added unsolved questions to the PR description. Please read.

@brandonchinn178
Copy link
Copy Markdown
Collaborator

  1. I think one or four lines makes sense. In other words, if it was originally written on one line, keep it one line (same as ifs today). If it's multiline (whether originally written on multiple lines or other formatting forcing it to be), it should be the full four lines
  2. Following the spirit of the previous point, I think it would be most intuitive to do
if cond1
  || cond2 then
  something
else
  something else

The multiline condition makes the overall expression four lines. I think it's fine that the indentation in the condition matches the block below, we already do this in a few places like instances with multiline constraints.

Copy link
Copy Markdown
Collaborator

@brandonchinn178 brandonchinn178 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Just let me know when you've resolved the open questions + added tests for them

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Jul 20, 2025

I think I solved problems 1 and 2. Sorry, I find merged condition and then-block with then keyword somewhere between unreadable.

Unfortunately, I don't like the code I wrote, I don't know Fourmolu code good enough. The style is great, but implementation sucks a little. I hope somebody will refactor it after me.

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Jul 20, 2025

Also, I'm not comfortable with the option name. one-level-ifs came from original patch, but I think indent-then-else :: Bool is better.

Copy link
Copy Markdown
Collaborator

@brandonchinn178 brandonchinn178 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments, take a look?

Also, I'm not comfortable with the option name. one-level-ifs came from original patch, but I think indent-then-else :: Bool is better.

indent-then-else doesn't quite communicate it to me either. How about compact-ifs?

thenBodySpan = getLocA then'
elseBodySpan = getLocA else'
isWholeOneLine = isOneLineSpanFromTo conditionSpan elseBodySpan
if isOneLineSpan conditionSpan then space else breakpoint
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use switchLayout with all the spans (condition/then/else), and that will set the context to be "SingleLine" or "MultiLine". Once this context is set, breakpoint will switch between space and newline automatically

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I can't. Could you help me?

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Aug 1, 2025

indent-then-else doesn't quite communicate it to me either. How about compact-ifs?

Which version is more compact? The proposed feature makes code narrower but sometimes taller. E.g. it may reformat 3-line block into 4-line.

@brandonchinn178
Copy link
Copy Markdown
Collaborator

I was thinking yours is more compact because it puts the if/then/else keywords on two lines instesd of three (in a multiline context)

Could also do shifted-ifs, since this format is shifting then up one line and shifting the bodies left one indentation

one-level-ifs or same-level-ifs is still fine for me, since it's putting then on the same line as if

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Aug 3, 2025

Could also do shifted-ifs, since this format is shifting then up one line and shifting the bodies left one indentation

Shift from what? Do you assume one of these style "old" or "default"?

From my taste, "then" on the same line with "if" is the neutral position, and "then" placed on a separate line and indented is shifted, because simply "indented" == "shifted".

@brandonchinn178
Copy link
Copy Markdown
Collaborator

Yes, the default Fourmolu format is the default because that's how 90% of Haskell code is formatted in the wild. So the option should be describing how we're transforming from that default style

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Aug 4, 2025

Oh, from that perspective, ok. I'm still not sure about the naming. I'd like leave the decision for the mainteiner.

@evanrelf
Copy link
Copy Markdown

evanrelf commented Aug 5, 2025

I noticed there are no test cases covering multiple chained if ... then ... else ...s, e.g.

if foo then
  doFoo
else if bar then
  doBar
else
  doBaz

This style ^ is what I would love to use with fourmolu, and I'm wondering if this one-level ifs PR will allow that?

@brandonchinn178
Copy link
Copy Markdown
Collaborator

@cblp I am the maintainer 🙂 Let's go with shifted-ifs.

Also, can you apply this diff? The key thing here is that we should avoid changing the code too much, to minimize conflicts when merging Ormolu changes back.

Expand to see diff
diff --git a/src/Ormolu/Printer/Meat/Declaration/Value.hs b/src/Ormolu/Printer/Meat/Declaration/Value.hs
index cc29dad9..0480a462 100644
--- a/src/Ormolu/Printer/Meat/Declaration/Value.hs
+++ b/src/Ormolu/Printer/Meat/Declaration/Value.hs
@@ -1101,6 +1101,8 @@ p_if ::
   LocatedA body ->
   R ()
 p_if placer render anns if' then' else' = do
+  oneLevelIfs <- getPrinterOpt poOneLevelIfs
+
   txt "if"
   space
   located if' p_hsExpr
@@ -1109,6 +1111,9 @@ p_if placer render anns if' then' else' = do
         where
           AnnsIf {aiThen, aiElse} = anns
 
+      locatedToken tokenSpan token =
+        located (L tokenSpan ()) $ \_ -> txt token
+
       betweenSpans spanA spanB s = spanA < s && s < spanB
 
       placeHangingLocated tokenSpan bodyLoc@(L _ body) = do
@@ -1121,33 +1126,23 @@ p_if placer render anns if' then' else' = do
         switchLayout [tokenSpan, bodySpan] $
           placeHanging placement (located bodyLoc render)
 
-  oneLevelIfs <- getPrinterOpt poOneLevelIfs
+      placeBranch tokenSpan body =
+        if oneLevelIfs && isOneLineSpan (getLocA body)
+          then placeHanging Normal (located body render)
+          else placeHangingLocated tokenSpan body
 
-  let placeBranch tokenSpan token body = do
-        located (L tokenSpan ()) $ \_ -> txt token
+  let hangIf m =
         if oneLevelIfs
-          then do
-            if isOneLineSpan $ getLocA body
-              then do
-                breakpoint
-                inci (located body render)
-              else placeHangingLocated tokenSpan body
-          else do
-            space
-            placeHangingLocated tokenSpan body
-
-      branches = do
-        placeBranch thenSpan "then" then'
-        breakpoint
-        placeBranch elseSpan "else" else'
-
-  if oneLevelIfs
-    then do
-      if isOneLineSpan $ getLocA if' then space else breakpoint
-      branches
-    else do
-      breakpoint
-      inci branches
+          then switchLayout [getLocA if'] breakpoint >> m
+          else breakpoint >> inci m
+  hangIf $ do
+    locatedToken thenSpan "then"
+    space
+    placeBranch thenSpan then'
+    breakpoint
+    locatedToken elseSpan "else"
+    space
+    placeBranch elseSpan else'
 
 p_let ::
   -- | True if in do-block

Can you also add @evanrelf's suggestion to the test suite?

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Aug 7, 2025

@evanrelf I think if-else-if chain is a good feature, but should be implemented separately

@brandonchinn178
Copy link
Copy Markdown
Collaborator

@cblp Can you ateast add a test so the behavior is documented?

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Aug 10, 2025

@brandonchinn178 I've added the patch.
@evanrelf I've added an if-else-if chain example to tests and examples.

Copy link
Copy Markdown
Collaborator

@brandonchinn178 brandonchinn178 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one TODO that was accidentally left in, but looks good otherwise!

Comment on lines +41 to +51
expr_chain =
if a then
b
else
if c then
d
else
if e then
f
else
g
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this looks fine for now. We can merge, but if someone else wants to put up a PR collapsing this, that would be welcome

poShiftedIfs = pure shiftedIfs
},
showTestCase = \(shiftedIfs, indent) ->
[ "if=" ++ renderPrinterOpt shiftedIfs,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: shifted=?

Requires regenerating output files

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@cblp
Copy link
Copy Markdown
Contributor Author

cblp commented Aug 10, 2025

@evanrelf I'm not sure aligning nested (chained) ifs is useful since we have MultiWayIf syntax.

Copy link
Copy Markdown
Collaborator

@brandonchinn178 brandonchinn178 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@brandonchinn178 brandonchinn178 merged commit 46a325c into fourmolu:main Aug 10, 2025
19 checks passed
@evanrelf
Copy link
Copy Markdown

Thanks for adding those example cases! Hopefully we can collapse those nested ifs in the future 😄

I'm not sure aligning nested (chained) ifs is useful since we have MultiWayIf syntax.

You're right that they both achieve the same semantic result, but syntactically they're meaningfully different. Nested ifs look like every other programming language, so they read more naturally for me. Also MultiWayIf adds two additional levels of indentation.

f =
  if foo then do
    before
    doFoo
    after
  else if bar then do
    before
    doBar
    after
  else do
    before
    doBaz
    after
f =
  if
    | foo -> do
        before
        doFoo
        after
    | bar -> do
        before
        doBar
        after
    | otherwise -> do
        before
        doBaz
        after

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python-styled ifs

4 participants