Min-Maxing Your Haskell Imports

Posted on June 3, 2019 by Robert Djubek


Explicit imports#

As you may well know it’s good practice when writing haskell to use qualified imports when appropriate as well as explicitly list what you import and export from modules. This is beneficial for someone else (which includes your future self) to understand where the functions and data types you’re using originate. It makes your code more approachable and maintanable. The problem is when you’re originally writing (or editing later) the code you don’t know what you’re going to need! This can be annoying.

How do we have our cake and eat it too? When we’re writing software we want easy access to imports (for both you and your editor for the sake of code completion), and when it’s “done” to use explicit imports… until the next time we work on it. ;)

Editors are your friends#

If you’re using an IDE or a proper editor (vim, emacs, etc.) with appropriate plugins ( ghcid, hindent, hlint ), various compiler flags, … your tools can and will help you with this problem. They might suggest what you need to import, or inform you of unnecessary imports you aren’t using. You’ll get various warnings and compile errors that prompt you along. You can also scan the code yourself and decide. You can start off by importing a whole module, and then adding () and fixing compile errors 1 by 1 adding to the list. Anyway, it’s tedious. Who has time for that?

More tools to make writing idiomatic code easier#

I’m a tool maker. I like tools. I especially like Tool. I might even be a tool. Most importantly I like making tools and having tools. I like fixing things and being efficient. and I’m a recovering perfectionist. If I can make a tool (either software or in meatspace) that makes a task easier, better, faster… I will. Sometimes the tool takes longer to make than the task to complete. However, the joy for me is often in making and using the tool. The tool that I made. Why does this matter to you? I made a few useful tools to further aid in solving the above problem. and I’m sharing them with you.

Enter minimal-imports and maximal-imports#

I.e., the inspiration of the title of this post. and this post. Yes, it’s an absurd title. Look where you are. :) Anyway, “Minimal imports” is actually a real thing that I didn’t totally just make up. Maximal imports (the name), however, is a joke. The corresponding program is serious. Naming is hard. Even Edward Kmett didn’t have a name for the concept that maximal-imports embodies.

This is what you’ve been eagerly anticipating while reading this robbish. There you will (currently) find two files. They will eventually be in a normal git repository. I have a shell.nix I use to setup the environment and a few other things I want to add. In the meantime you can compile them ghc -O2 filename.hs and put them on your path. If you find problems let me know and I’ll try to fix. They’re quite primitive and unrefined at this point. They’re probably not even good. I hacked them together over “a few” sleep deprived hours over night, got them to work, and that’s about where they are right now. Then again, what they do isn’t particularly complicated, either. Ironically they’re probably not idiomatic. But the imports serve as examples of the to and from.

How it’s used#

In your proper editor (vim, doom-emacs, spacemacs, evil-mode, …) go into visual mode, select all of your imports, only your imports (with 2 caveats), and type one of

:!minimal-imports
:!maximal-imports

make sure you have saved recently in case something goes wrong… it shouldn’t go wrong, but… there are no guarantees, there is no warranty. :)

Depending on which one you use you will either end up with your imports replaced with the minimal amount of imports that explicitly list everything, or maximal imports that import entire modules. You may also end up with no modifications to your imports in certain circumstances (see the caveats) Maximal-imports can potentially cause problems if shadowing occurs between modules. Just fix that import (use qualified imports or what not) if GHC complains.

This is surely possible to use in a subset of other editors but I’m not sure how to because I don’t use them. If you figure it out let me know and I’ll update the instructions.

How it works#

The vim command sends the selected text to the program over stdin/stdout. The program sends text back after modifying it. Vim replaces the text you selected with the text the program sends over the pipe. The program does a bit of hackish checking to determine what is an import and what is not. Minimal-imports will attempt to find a file that contains the necessary imports. Explained below.

How to use#

for minimal-imports to work you must have done two things

these are the caveats

  • You have to have compiled your program with the GHC flag -ddump-minimal-imports. The more recently the better or you might not end up with the right imports.
  • You must have a single line comment -- M.imports selected above, and along, with your imports when you run the command. M.imports is named after your module. The file src/Site/Slug.hs would need -- Site.Slug.imports If either of those conditions is not met it won’t work. You’ll (well, should) merely end up with your imports unchanged. Maybe an extra new line. No big deal.

The idea, the inspiration, the implementation#

While making this site, in fact, I was tired of doing all of this by hand. I asked around if there was a way to make it easier. The original question. Ed Kmett told me about -ddump-minimal-imports. As well as that there’s another tool that makes use of that flag but it does not do the same thing. He couldn’t remember the name at the time but I later found out, obviously. Anyway, that was a start.

The hard part already exists#

If you’re not familiar with it beyond the mentioning above -ddump-minimal-imports is a GHC flag that causes the compiler to create the files with the form M.imports. (where M is the module name) The contents of the file are the explicit imports that we desire in our final project.

My first attempt#

#!/bin/sh

SCRIPT=$(realpath "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
ROOTPATH="$(cd "$SCRIPTPATH"/../ && pwd)"

for i in "$ROOTPATH"/dist-newstyle/build/x86_64-linux/ghc-8.6.4/*/*/*/build/site/site-tmp/*.imports; do
        printf "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n%s\n\n" "$i"
	basename "$i"
	printf "\n"
	cat "$i";
done

printf "\n\n\n"

for i in "$ROOTPATH"/dist-newstyle/build/x86_64-linux/ghc-8.6.4/*/*/*/build/site/site-tmp/*.imports; do
	basename "$i" ;
done

(I don’t know shell if it’s bad that’s why) Obviously, while this makes finding and seeing the imports -ddump-minimal-imports easier, it still requires a lot of manual intervention and copy-paste. It so happens to be easily automated… as you’ve seen in this very post. I also didn’t notice there was a flag to set where they get dumped….

Second attempt, editor integration!#

There’s a good blog post by Gabriel Gonzalez that goes through and explains the implementation of a vim plugin that aligns equals signs. I won’t use it because I prefer running hindent on whole files to avoid bikeshedding about formatting and decision paralysis. It’s one less thing I have to worry about if I let hindent decide the final format. I can focus on getting the code to work and let hindent indent it all proper like and consistent. Anyway, the idea was largely applicable (especially in maximal-imports, just look at it…) to the task at hand.

Anyway, combine all of that together and ta da; here we are.