Dhall to HLint: Using Dhall to generate HLint rules

This short blog post covers the process of creating the .hlint.yaml file from the Dhall configuration.

Motivation

Let’s first figure out why one could need custom HLint rules. The default HLint settings mostly cover base library. However, nowadays many people are using alternative preludes. Having .hlint.yaml file specific to the prelude library can help to migrate to the alternative prelude and use it efficiently.

Our Kowainik organisation is not an exception, and we also have our custom prelude – relude. We are working a lot to make it very friendly to those who are using it. relude functionality has a few differencies from what standard Prelude gives us, which means that not all out-of-the-box HLint rules are suitable for us. That’s why we need our own rules to provide more information about relude just by running hlint on your project that uses relude as a custom prelude. To show how helpful it could be I list use cases for custom HLint rules below.

To summarise everything said before, we have to write a lot of boilerplate to cover all these rules which we (as lazybones) would like to avoid at all costs. Moreover, the maintenance price is quite high for thousands of lines of yaml.

Why Dhall

As the tool that can help us with removing boilerplate, we have chosen Dhall language. As reference:

Dhall is a programmable configuration language that is not Turing-complete. You can think of Dhall as: JSON + functions + types + imports

This sounds like pretty much what we need.

You can wonder, why we are not using Haskell for such purposes (though we love it so much ♥). The answer is that we don’t need IO capabilities for our problem; totality and safety of Dhall are enough here. Changing the configuration in Haskell requires to recompile whole program before generating config, but with Dhall there’s no such extra step. Not to mention the very nice string interpolation that Dhall has. Also, we would like to learn about new technologies, and this seems like an excellent opportunity to dive into Dhall.

Implementation

Basically, HLint file is just a list of different kind of rules. List in Dhall should consist of the elements of the same type (because Dhall is the typed configuration language), but as you’ve seen we need to use warn, ignore, hint and other rules. To unify different rule types with the same type we can create sum type in Dhall. Here how you do it (this is the hlint/Rule.dhall file):

This type might look like this in Haskell:

Okay, once we’ve introduced the type, we can create functions for easy rules addition. I’m going to show the one for the reexport warnings. The rest can be found in hlint/warn.dhall.

We need to implement a Dhall function that takes a function/type name and the module from which people can export it because they don’t know that it’s already in relude and I expect this function to output the HLint warn about this redundant import. Let’s try to do this. As Haskell analogue, we need function like warnReexport :: Text -> Text -> Rule (in our case the rule is RuleWarn).

And, the most important part is how actually to use this function. Lets look at hlint/hlint.dhall.

And the same with all other rules, pretty easy.

To generate .hlint.yaml from hlint.dhall you can run the following command (you need to have dhall-json installed):

$  dhall-to-yaml --omitNull <<< './hlint/hlint.dhall' > .hlint.yaml

Conclusion

The process of creating Dhall modules was not very painful, so we can consider the experiment successful and we are going to maintain the .hlint.yaml using our hlint.dhall configuration. Now, adding additional rule is just one line, and it’s much easier to refactor configuration!

Here is the table of file size comparison in different formats to show you how many keystrokes we managed to avoid.

.hlint.yaml hlint.dhall
Lines 2986 883