ennocramer / floskell

Floskell is a flexible Haskell source code pretty printer.
BSD 3-Clause "New" or "Revised" License
178 stars 22 forks source link

Parts of expression should be indented relative to beginning of expression #11

Open gittywithexcitement opened 5 years ago

gittywithexcitement commented 5 years ago

Hi there! First, I love what you've created! Thank you!

My issue is that some infix operators, when part of an expression that spans multiple lines, can result in subsequent lines being indented less than the first line. Here's an example:

func :: Seq Int
func = let foobar = makeASequence undefined undefined undefined
             <> makeASequence undefined undefined undefined
       in foobar

makeASequence :: a -> b -> c -> Seq Int
makeASequence = undefined

I don't like that the <> makeASequence is indented less than the first makeASequence. At first glance, I think it misleads the mind into thinking that the second line is unrelated to the first. And it's different from normal function application, where I have an option to make arguments on subsequent lines align with the first argument; I can't do anything like that here.

Here's what I would like:

func :: Seq Int
func = let foobar = makeASequence undefined undefined undefined
                      <> makeASequence undefined undefined undefined
       in foobar

I think that a continuation of an expression should be at least as indented as the beginning of that expression, or more indented. Note that <> seems to be known by floskell as a base infix operator.

I'd be fine with an option that allowed for controlling this: A) align subsequent lines with the first B) indent them relative to the first by some constant I think this option would fit right in with your existing option section, indent.

I tried changing all the obvious options in my floskell.json to see if any of them would achieve what I wanted. I also tried all the built-in styles. No luck. Hopefully I didn't overlook an already existing option.

ennocramer commented 5 years ago

There is currently no option to change the alignment as you want.

At the moment, Floskell treats declarations inside let the same as top level declarations or declarations in where blocks. In particular, the right-hand-side of the declaration will be indented by the onside indent relative to the declaration itself, when it extends over one line, but no additional indentation is applied to subsequent lines. In your case, onside indent seems to be two spaces, applied relative to foobar =.

I'm would like to understand exactly what option you're asking for. I see three possibilities at the moment

1) Infix operators should indent based on the start of the first argument, 2) The right-hand-side of all declarations should indent based on its start, not based on the start of the declaration, or 3) The right-hand-side of declarations in let only should indent based on its start.

For options one and two, I would agree that a configuration option would fit in the design of Floskell. Option three feels a bit much of a special case, but might be implemented as a style option.

gittywithexcitement commented 5 years ago

I think something more like #2 is what I'm looking for. However, perhaps my request is more complex than what we've described so far.

I think I'd say that for any expression (e.g. an infix expression, a normal function application, a case expression, etc), the 'subparts' of that expression, when they are moved to subsequent lines, are indented relative to the beginning of the expression. I think this rule should apply hierarchically, when expressions are nested inside each other.

Here's another example, using case. Let's assume cases get indented by 2. Undesirable, what floskell does today:

example = foo $ case bar of
    One   -> apple
    Two   -> banana
    Three -> cherry

I'd like the cases that follow of to be indented relative to case. Desired:

example = foo $ case bar of
                  One   -> apple
                  Two   -> banana
                  Three -> cherry

Here's a more complex example. Assume the column limit is 80 columns. If I make foo really long, then case should be moved to its own line (and it should be indented relative to foo) so that the cases can be indented relative to case:

Undesirable, what floskell does today:

example =
  fooIsReallyLooooooooooooooooooooooooooooooooooooooooooooooooong $ case bar of
      One   -> apple
      Two   -> banana
      Three -> cherry

desired:

example =
  fooIsReallyLooooooooooooooooooooooooooooooooooooooooooooooooong $ 
    case bar of
      One   -> apple
      Two   -> banana
      Three -> cherry

Here's another example, where we have several levels of nesting (e.g. the case is nested as an argument of anotherLongFunction, which is itself nested inside someFunction):

Undesirable, what floskell does today:

example = someFunction firstArg
                       (anotherLongFunction $ case bar of
                            One   -> apple
                            Two   -> banana
                            Three -> cherry)

desired:

example = someFunction firstArg
                       (anotherLongFunction $ case bar of
                                                One   -> apple
                                                Two   -> banana
                                                Three -> cherry)

I think I've heard this idea, or something similar, called "the rectangle rule". Essentially, for every expression, draw the smallest possible rectangle that includes all parts of that expression; it should include no other expressions. I would identify the right hand side of any let binding/do binding/where binding/etc. as an expression.

All of the examples given above violate the rule because the cases are less indented than case, which means whatever is to the left of case is getting included in the minimal rectangle (e.g. anotherLongFunction is in the rectangle).

It's probably helpful to look at some other examples... but this comment has gotten long enough.

ennocramer commented 5 years ago

Thanks for the detailed explanation. I agree with you that this would fit nicely as an option for Floskell.