Skip to main content

Reader-Friendly Haskell Imports

This article describes some practices that I follow to make my Haskell code easier to read. In general, these practices allow readers to determine where types, functions, etc. are defined without having to use an IDE or other tooling. In particular, the benefits are as follows:

  • The code is easier to read online, without having to load it into an IDE. For example, many people read source on GitHub.
  • The code is easier to read in a text editor when not using an IDE such as HLS. Some people, me included, prefer to not use an IDE.
  • The code is easier to read in print.

While all the above benefits apply to beginners and experts alike, note that friendly imports are especially helpful to beginners and new team members who are seeing a large codebase for the first time. Some people find it tedious to follow these practices, but I think it is wise to optimize for readability and maintainability, not the number of characters.

Only Import Explicitly

All imports should either be qualified or have an explicit list of names to import.

Here is an example of an import with an explicit list of names:

import Data.Maybe (fromMaybe, listToMaybe)

Here is an example of a qualified import:

import qualified System.Process as Proc

In some cases, an explicit list of names may be imported from a module that is also imported qualified. Here is an example where the Text type is imported explicitly:

import qualified Data.Text as T
import Data.Text (Text)

Another common case is importing operators explicitly so that they do not need to be qualified when used:

import qualified System.FilePath as FilePath
import System.FilePath ((</>))

When reading the code, it is then easy to determine where something is defined. For example, name T.unpack clearly comes from the module that is imported qualified as T (Data.Text). In general, unqualified names should either be explicitly listed in an import or defined in the current module.

Unqualified imports of everything in a module should be avoided because it can be difficult for the reader to determine where a name comes from. For example, an import like the following should be avoided:

import Data.Maybe

I would like to also mention a personal convention that I follow. When I import from a module defined in the base package, I usually import an explicit list of names. I rare import these packages qualified, though there are exceptions, such as importing Data.List qualified in order to provide consistency when other data structure modules are used in the same code.

Domain Specific Languages

Haskell is a convenient host language for writing domain-specific languages (“DSLs”). Sometimes a DSL is best used with unqualified imports of everything in a module, as explicitly listing a large number of names can be tedious. Note, however, that it can be even more tedious for a reader to read the code without the assistance of an IDE in cases where it is not clear where names are imported from.

Re-Exports

Re-exporting names from other modules can make it very difficult for readers to follow code. Sometimes it is necessary to re-export code due to the use of internal modules and the avoidance of import loops. In this case, just be sure to provide good API documentation. If a user loads the documentation for a module only to find that is re-exports many other modules, with no documentation or clues about what comes from where, it can be very frustrating.

Avoid re-exporting names just so that users do not have to import from separate modules. Doing so results in typing fewer characters, but it can significantly hurt the readability/usability of the package.

ImportQualifiedPost

Some people dislike the syntax of qualified imports, where the qualified keyword comes before the module name. Note that the ImportQualifiedPost extension can be used to fix this issue when using GHC 8.10.1 or newer.

Package Documentation

The above practices allow readers to easily determine which module a name is imported from, but they do not help determine which package the module is defined in. Adding documentation to tell users where modules are defined can make code much easier to follow, especially in packages that have many dependencies.

I group my imports by package name, sorted in alphabetical order, with a comment that links to the Hackage documentation for the package. Here is an example from some code that is on my screen as I write this:

-- https://hackage.haskell.org/package/base
import Control.Applicative ((<|>))
import Control.Monad (forM)
import Data.List (find, sortOn)
import Data.Maybe (listToMaybe)
import Data.Ord (Down(Down))

-- https://hackage.haskell.org/package/bytestring
import qualified Data.ByteString as BS
import Data.ByteString (ByteString)

Readers can easily open the URL and see the documentation, with no searching (or IDE) required.

I put imports from the current package last, with the package name in parenthesis as follows:

-- (example)
import qualified Example.Demo as Demo

When working on proprietary software, the team can determine the conventions to use for internal packages. For example, imports from internal packages can be put in between the Hackage packages and the current package as follows:

-- https://hackage.haskell.org/package/unordered-containers
import qualified Data.HashMap.Strict as HashMap

-- acme-missile-launcher
import qualified Acme.Launcher.Unsafe as Unsafe

-- (example)
import qualified Example.Demo as Demo

PackageImports

The PackageImports extension allows imports to qualify the package name in GHC 6.10.1 or later. This is another way to document which package a module is imported from.

Author

Travis Cardwell

Published

Tags