Haskell Style Guide

Style guide used in Kowainik.

This document is a collection of best-practices inspired by commercial and free open source Haskell libraries and applications.

Style guide goalsπŸ”—

The purpose of this document is to help developers and people working on Haskell code-bases to have a smoother experience while dealing with code in different situations. This style guide aims to increase productivity by defining the following goals:

  1. Make code easier to understand: ideas for solutions should not be hidden behind complex and obscure code.
  2. Make code easier to read: code arrangement should be immediately apparent after looking at the existing code. Names of functions & variables should be transparent and obvious.
  3. Make code easier to write: developers should think about code formatting rules as little as possible. The style guide should answer any query pertaining to the formatting of a specific piece of code.
  4. Make code easier to maintain: this style guide aims to reduce the burden of maintaining packages using version control systems unless this conflicts with the previous points.

Rule of thumb when working with existing source codeπŸ”—

The general rule is to stick to the same coding style that is already used in the file you are editing. If you must make significant style modifications, then commit them independently from the functional changes so that someone looking back through the changelog can easily distinguish between them.

IndentationπŸ”—

Indent code blocks with 4 spaces.

Indent where keywords with 2 spaces and always put a where keyword on a new line.

Line lengthπŸ”—

The maximum allowed line length is 90 characters. If your line of code exceeds this limit, try to split code into smaller chunks or break long lines over multiple shorter ones.

WhitespacesπŸ”—

No trailing whitespaces (use some tools to automatically cleanup trailing whitespaces).

Surround binary operators with a single space on either side.

AlignmentπŸ”—

Use comma-leading style for formatting module exports, lists, tuples, records, etc.

If a function definition doesn’t fit the line limit then align multiple lines according to the same separator like ::, =>, ->.

Align records with every field on a separate line with leading commas.

Align sum types with every constructor on its own line with leading = and |.

  • The indentation of a line should not depend on the length of any identifier in preceding lines.

Try to follow the above rule inside function definitions but without fanatism:

Basically, it is often possible to join consequent lines without introducing alignment dependency. Try not to span multiple short lines unnecessarily.

If a function application must spawn multiple lines to fit within the maximum line length, then write one argument on each line following the head, indented by one level:

NamingπŸ”—

Functions and variablesπŸ”—

  • lowerCamelCase for function and variable names.
  • UpperCamelCase for data types, typeclasses and constructors.

Try not to create new operators.

Do not use ultra-short or indescriptive names like a, par, g unless the types of these variables are general enough.

Do not introduce unnecessarily long names for variables.

For readability reasons, do not capitalize all letters when using an abbreviation as a part of a longer name. For example, write TomlException instead of TOMLException.

Unicode symbols are allowed only in modules that already use unicode symbols. If you create a unicode name, you should also create a non-unicode one as an alias.

Data typesπŸ”—

Creating data types is extremely easy in Haskell. It is usually a good idea to introduce a custom data type (enum or newtype) instead of using a commonly used data type (like Int, String, Set Text, etc.).

type aliases are allowed only for specializing general types:

Use the data type name as the constructor name for data with single constructor and newtype.

The field name for a newtype must be prefixed by un followed by the type name.

Field names for the record data type should start with the full name of the data type.

It is acceptable to use an abbreviation as the field prefix if the data type name is too long.

CommentsπŸ”—

Separate end-of-line comments from the code with 2 spaces.

Write Haddock documentation for the top-level functions, function arguments and data type fields. The documentation should give enough information to apply the function without looking at its definition.

Use block comment style ({- | and -}) for Haddock for multiple line comments.

For commenting function arguments, data type constructors and their fields, you are allowed to use end-of-line Haddock comments if they fit line length limit. Otherwise, use block style comments. It is allowed to align end-of-line comments with each other. But it is forbidden to use comments of different styles for the function arguments, data type constructors, and fields.

If possible, include typeclass laws and function usage examples into the documentation.

Guideline for module formattingπŸ”—

Allowed tools for automatic module formatting:

{-# LANGUAGE #-}πŸ”—

Put OPTIONS_GHC pragma before LANGUAGE pragmas in a separate section. Write each LANGUAGE pragma on its own line, sort them alphabetically and align by max width among them.

You can put commonly-used language extensions into default-extensions in the .cabal file. Here is the list of extensions this style guide allows one to put in there:

Export listsπŸ”—

Use the following rules to format the export section:

  1. Always write an explicit export list.
  2. Indent the export list by 7 spaces (so that the bracket is below the first letter of the module name).
  3. You can split the export list into sections. Use Haddock to assign names to these sections.
  4. Classes, data types and type aliases should be written before functions in each section.

ImportsπŸ”—

Always use explicit import lists or qualified imports. Use qualified imports only if the import list is big enough or there are conflicts in names. This makes the code more robust against changes in dependent libraries.

  • Exception: modules that only reexport other entire modules.

Imports should be grouped in the following order:

  1. Non-qualified imports from Hackage packages.
  2. Non-qualified imports from the current project.
  3. Qualified imports from Hackage packages.
  4. Qualified imports from the current project.

Put a blank line between each group of imports.

Put 2 blank lines after the import section.

The imports in each group should be sorted alphabetically by module name.

Data declarationπŸ”—

Refer to the Alignment section to see how to format data type declarations.

Records for data types with multiple constructors are forbidden.

StrictnessπŸ”—

Fields of data type constructors should be strict. Specify strictness explicitly with !. This helps to avoid space leaks and gives you an error instead of a warning in case you forget to initialize some fields.

DerivingπŸ”—

Type classes in the deriving section should always be surrounded by parentheses. Don’t derive typeclasses unnecessarily.

Use -XDerivingStrategies extension for newtypes to explicitly specify the way you want to derive type classes:

Function declarationπŸ”—

All top-level functions must have type signatures.

All functions inside a where block must have type signatures. Explicit type signatures help to avoid cryptic type errors.

You might need the -XScopedTypeVariables extension to write the polymorphic types of functions inside a where block.

Surround . after forall in type signatures with spaces.

If the function type signature is very long, then place the type of each argument under its own line with respect to alignment.

If the line with argument names is too big, then put each argument on its own line and separate it somehow from the body section.

In other cases, place an = sign on the same line where the function definition is.

Put operator fixity before operator signature:

Put pragmas immediately following the function they apply to.

In case of data type definitions, you must put the pragma before the type it applies to. Example:

if-then-else clausesπŸ”—

Prefer guards over if-then-else where possible.

When writing monadic code in do-blocks where guards cannot be used, add one indentation level before then and else:

In the code outside do-blocks you can align if-then-else clauses like you would normal expressions:

Case expressionsπŸ”—

Align the -> arrows in the alternatives when it helps readability.

Use the -XLambdaCase extension when you perform pattern matching over the last argument of the function:

let expressionsπŸ”—

Write every let-binding on a new line:

Put a let before each variable inside a do block. In pure functions try to avoid let. Instead, use where.

General recommendationsπŸ”—

Try to split code into separate modules.

Avoid abusing point-free style. Sometimes code is clearer when not written in point-free style:

Prefer pure over return.

Code should be compilable with the following ghc options without warnings:

  • -Wall
  • -Wincomplete-uni-patterns
  • -Wincomplete-record-updates
  • -Wcompat
  • -Widentities
  • -Wredundant-constraints
  • -Wmissing-export-lists
  • -Wpartial-fields

Enable -fhide-source-paths and -freverse-errors for cleaner compiler output.

Use -XApplicativeDo in combination with -XRecordWildCards to prevent position-sensitive errors where possible.