Skip to content

Instantly share code, notes, and snippets.

@androidfred
Last active August 14, 2022 05:11
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save androidfred/66873faf9f0b76f595b5e3ea3537a97c to your computer and use it in GitHub Desktop.
Save androidfred/66873faf9f0b76f595b5e3ea3537a97c to your computer and use it in GitHub Desktop.
Use semantic indenting

Use semantic indenting

TLDR - Summary

Semantic indenting can greatly increase readability and maintainability of code. It can even increase the quality of code! (eg by making excessive dependencies more obvious etc)

Longer version

How many times have you come across code indented like this (ignore the code, it's just gibberish, focus on the indenting itself)

    @Autowired
    public FooService(FirstDependency first, SecondDependency second, ThirdDependency third,
            FourthDependency fourth, FifthDependency fifth, SixthDependency sixth,
            SeventhDependency seventh) {
        //...
    }
    
    @Override
    public List<Thingos> getThingos(int number, Context context, 
            String anotherArgument) {
        if (theSky.getColor().equals("blue") ||
                        (theSky.getColor().equals("red") 
        && featureSwitch.isEnabled(AppFeature.OUR_FEATURE))) {
            doSomething(number, context, anotherArgument, "some other argument",
                number * 2);
        } else {
            //...
        }
    }
}

Even in this short example, it takes a lot of effort just to parse what code is at what semantic level. Code indented like this is needlessly confusing, and the more code, and the more nested the code, the bigger the problem.

Consider instead if semantic indenting was used

    @Autowired
    public FooService(
            FirstDependency first, 
            SecondDependency second, 
            ThirdDependency third,
            FourthDependency fourth, 
            FifthDependency fifth, 
            SixthDependency sixth,
            SeventhDependency seventh
    ) {
        //...
    }

    @Override
    public List<Thingos> getThingos(
            int number, 
            Context context, 
            String anotherArgument
    ) {
        if (
                theSky
                    .getColor()
                    .equals("blue") 
        || (
                theSky
                    .getColor()
                    .equals("red") 
                && featureSwitch
                    .isEnabled(AppFeature.OUR_FEATURE)
            )
        ) {
            doSomething(
                    number, 
                    context,
                    anotherArgument, 
                    "some other argument",
                    number * 2
            );
        } else {
            //...
        }
    }
}

Now, it's immediately obvious what code is at what semantic level. Especially, the risk of confusing what level the || and && is at in the if statement is basically eliminated.

There are too many linebreaks

Yes, there probably are. That's a smell that the class and its constructor and methods probably have too many dependencies and arguments. The fact that we can easily see that now is a benefit of semantic indenting, not a problem - without semantic indenting, the same problem is there, it's just obfuscated.

To be clear, even in code that doesn't have too many dependencies and arguments, semantically indented code definitely does result in more lines - the code starts to look more like SQL than imperative instructions. I believe this can subtly encourage a more declarative, functional coding style, which I consider a benefit. (others may disagree)

It doesn't adhere to Java standard indenting

No, it doesn't. Java's standard indenting looks like this

public List<Thingos> getThingos(int number,
		                Context context,
		                String anotherArgument) { 
    //...		                        
}

//things that are really at the same semantic level ending up in different places
public List<Thingos> anotherMethodWithAMuchLongerName(int number,
		                                      Context context,
		                                      String anotherArgument) { 
    //... 
}


        List<Integer> myListOfNumbers = Arrays.asList(1, 2, 3);
        myListOfNumbers.stream()
                .filter(number -> number > 0) //indent second call
                .map(String::valueOf)
                .collect(Collectors.toList());

with semantic indenting, that would look like this

public List<Thingos> getThingos(
    int number,
    Context context,
    String anotherArgument
) { 
    //...		                        
}

public List<Thingos> anotherMethodWithAMuchLongerName(
    int number,
    Context context,
    String anotherArgument
) { 
    //... 
}

        List<Integer> myListOfNumbers = Arrays.asList(1, 2, 3);
        myListOfNumbers
                .stream() //indent first call
                .filter(number -> number > 0)
                .map(String::valueOf)
                .collect(Collectors.toList());

which, if you think about it, really makes more sense from an indenting point of view.

And... there's really nothing wrong with deviating from a standard if you have good reason to. There are countless examples of things that used to be standard in Java (and other languages) that people realized weren't the best way to do things, and over time, the different way of doing them became the new standard.

These days screens are wider than 80 characters, we should use that

Fair point, but long lines using the full width of the screen are still best used only when the statement a) fits on a single line and b) doesn't have too many semantic levels. (even statements that fit on a single line can sometimes have too many semantic levels)

eg, this is fine!

every { user.permissions } returns setOf(Permission.USER_SEARCH_BY_USER_ID, Permission.USER_SEARCH_BY_USER_IDS)

whereas this is already less fine (to be clear, it's still fairly easy to read, but when there's lots of lines like that, it quickly becomes increasingly annoying to read the code)

every { user.permissions } returns setOf(Permission.USER_SEARCH_BY_USER_ID, Permission.USER_SEARCH_BY_USER_IDS, 
    Permission.USER_SEARCH_BY_EMAIL, Permission.USER_SEARCH_BY_PROPERTY)

and could arguably use semantic indenting like this

every { 
    user.permissions 
} returns 
        setOf(
            Permission.USER_SEARCH_BY_USER_ID, 
            Permission.USER_SEARCH_BY_USER_IDS, 
            Permission.USER_SEARCH_BY_EMAIL, 
            Permission.USER_SEARCH_BY_PROPERTY
        )

Worth mentioning also is that, while we have lots of screen real estate in our IDEs, and with our external monitors in the office, when working with just a laptop screen, or especially in the GitHub split window pull request review GUI, that's not always the case. (GitHub will actually make poorly indented code even worse by inserting soft linebreaks)

Isn't this just a pointless argument about cosmetics, like tabs vs spaces?

Granted, indenting really is literally just cosmetics, and I do think it's pointless to be fundamentalist about it and fight about it.

But I think most people would agree that not giving any thought to indenting is objectively bad, irrespective of which style or standard you prefer. And, as I hope I've shown, indenting can actually reveal underlying real problems with the code.

Also, note that I'm not making some fundamentalist argument that absolutely everything must be semantically indented! Semantically indenting absolutely everything, like this

List<Integer> myListOfNumbers = Arrays
                                    .asList(
                                            1, 
                                            2, 
                                            3
                                    );

would definitely be excessive and make the code harder to read, not easier. Use your judgement!

@stefanoborini
Copy link

Honestly, I believe your first snippet to be much more readable than the second. Humans are good at understanding patterns. Those arguments may be on the same line because they are "together" conceptually (e.g. line_width, line_length for the first line aggregating geometric parameters, fill_color, line_color for the second to aggregate color parameters etc)

There has been a trend, with automated formatters such as gofmt and black, to destroy compact, perfectly readable code to favour machine and diff-oriented formatting, with the idea that "you must have the shortest diff possible". But you read code all the time, you read diffs only once, at review.

This kind of formatting literally makes my eyes bleed. It destroys important hints, it lengthens the code, and forces you to read code like you would read

a

sentence

written

almost

line

by line

like

this

@Simulacrum0
Copy link

Turning code lines, particularly "long parameter sequences" into columns does seem far easier to understand, to tweak with minor changes, and follow the flow. i generally split my text-editors into a series of vertical columns for program flows that jump in and out of many functions and this approach has proven valuable/useful since we've gotten beyond the SVGA resolution limits nearly 20years ago. Reading "stefanoborini"'s comment above reminds me that humans are built on strong preferences, which means different approaches will be the best tool for different people. i think our coding-community would do well to label this phenomena ( 'neuro-diversity'? 'People-r-Unique'? 'Personalization-Matters?' ) and offer methods in such a vein...and in particular, support auto-reformatting of code per individual and storage of code differently ( git-Diff/shared-repos)

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