A Logical Way to Split Long Lines
samwho keyboard logo

A Logical Way to Split Long Lines

Splitting long lines is something we do every day as programmers, but rarely do I hear discussion about how best to do it. Considering our industry-wide obsession with “best practices,” line breaks have managed to stay relatively free from scrutiny.

A few years ago, I learned a method for splitting lines that is logical, language-independent and, most importantly, produces good results.

# The Rectangle Method

The core principal of this method is to always make sure you can draw a rectangle around an element and all of its children, without having to overlap with any unrelated elements. The outcome is that related things stay closer together, and our eyes rarely have to dart between distant locations.

Confused? So was I. Let’s walk through an example.

JavacParser parser = parserFactory.newParser(javaInput.getText(), /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);

This line is 139 characters long and was taken from the source code of google-java-format. It is composed of a number of elements:

  1. A variable declaration. This encompasses the entire line.
  2. The variable declaration splits in to two halves: the type and name on the left hand side, and the expression on the right hand side.
  3. The expression is a single method call, which could be split in to the receiver, method name, and its arguments.
  4. Lastly, each method argument is its own element. Comments included.

It’s easy to draw a rectangle around this, it’s just one line. But if we say that our maximum allowed line length is 80, this line is a bit too long and needs to be split.

# Deciding where to make the first split

What do we mean when we say “an element and all of its children?” Programming language syntax can usually be represented as a tree. Our example would look something like this:

        Variable declaration
       /                    \
 Type + name             Expression
                        /          \
                   Receiver     Method call
                               /           \
                            Name         Arguments

We want to split such that you can draw a rectangle around each subtree, without touching any other subtree.

A natural place to make this first split would be just after the =:

JavacParser parser =
    parserFactory.newParser(javaInput.getText(), /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);

This passes the rectangle test because we can draw a rectangle around every element and its children without overlapping with unrelated elements:

┌──────────────────────────────────────────────────────┐
│┌──────────────────────┐                              │
││ JavacParser parser = │                              │
│└─┬────────────────────┴─────────────────────────────┐│
│  │ parserFactory.newParser(javaInput.getText(), ... ││
│  └──────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────┘

One big rectangle around the whole thing, and two smaller rectangles around each the declaration and assignment. Note that no rectangle overlaps any other rectangle.

It’s good progress, but the second line is still 118 characters long so needs splitting again.

Before doing this, I want to show how this would look if we split a little differently:

JavacParser parser = parserFactory.newParser(
  javaInput.getText(), /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);

This doesn’t pass the rectangle test:

                     ┌────────────────────────────────┐
JavacParser parser = │ parserFactory.newParser(       │
┌────────────────────┘                                │
│ javaInput.getText(), /*keepDocComments=*/ true, ... │
└─────────────────────────────────────────────────────┘

It’s not possible to draw a rectangle around the right hand side of the = and catch all of its children without also catching the left hand side of the =. It’s correct that the rectangle method flags this as a bad split. There’s an awful long way to travel from newParser to its first argument, which might result in your eyes having to dart back and forth more than necessary.

# Deciding where to make the second split

There are a couple of ways to make the second split, but the one I would go with is this:

JavacParser parser =
    parserFactory.newParser(
        javaInput.getText(), /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);

Let’s see how that looks with some rectangles around it:

┌────────────────────────────────────────────────────────────┐
│┌──────────────────────┐                                    │
││ JavacParser parser = │                                    │
│└─┬────────────────────┴─────────────────────────────┐      │
│  │ parserFactory.newParser(                         │      │
│  └─┬────────────────────────────────────────────────┴────┐ │
│    │ javaInput.getText(), /*keepDocComments=*/ true, ... │ │
│    └─────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘

We’re still good! Note that the above doesn’t draw a rectangle around every element we could, mostly due to space limitations. You can also draw a rectangle from parserFactory.newParser around everything else after it.

Another way of doing the split that would also pass the rectangle test is:

JavacParser parser =
    parserFactory
        .newParser(
            javaInput.getText(), /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);

But splitting at the . feels a little too eager to me. You can use less vertical space and lose no clarity by leaving that line as one.

Sadly our third line is still 94 characters long, and needs to be split yet again.

# Deciding where to make the third split

Again, there are multiple routes for this one but I would go for the following:

JavacParser parser =
    parserFactory.newParser(
        javaInput.getText(),
        /*keepDocComments=*/ true,
        /*keepEndPos=*/ true,
        /*keepLineMap=*/ true);

With rectangles:

┌──────────────────────────────────┐
│┌──────────────────────┐          │
││ JavacParser parser = │          │
│└─┬────────────────────┴─────┐    │
│  │ parserFactory.newParser( │    │
│  └─┬────────────────────────┴───┐│
│    │ javaInput.getText(),       ││
│    ├────────────────────────────┤│
│    │ /*keepDocComments=*/ true, ││
│    ├────────────────────────────┤│
│    │ /*keepEndPos=*/ true,      ││
│    ├────────────────────────────┤│
│    │ /*keepLineMap=*/ true);    ││
│    └────────────────────────────┘│
└──────────────────────────────────┘

Again, for space reasons, not all possible rectangles have been drawn.

We could have also had multiple arguments per line:

JavacParser parser =
    parserFactory.newParser(
        javaInput.getText(), /*keepDocComments=*/ true,
        /*keepEndPos=*/ true, /*keepLineMap=*/ true);

This passes the rectangle test and none of the lines go past the 80 character limit. However, I usually avoid this as a matter of personal preference.

# Conclusion

Most of my code follows this style, and I feel it’s easier to read as a result. I’m sure this is just one of many approaches, and I would love to hear about them and how they compare to this one!

powered by buttondown