Haskell Style Guide

This is my personal style guide for writing Haskell code. As with all style guides, this guide is incomplete, fallible, and its recommendations aren't appropriate for every circumstance.

I've only recently started writing this, so it's obviously particularly incomplete. Hopefully, it will grow as I learn and think more!

This guide is focused only on recommendations that aren't universal across programming languages, for such recommendations see my Programming Style Guide. Additionally, I don't currently publish any recommendations that concern third-party libraries.

Its intended audience is intermediate or advanced Haskell programmers who endeavor especially to write reliable, correct code. In particular, I'm often willing to err on the side of reliability over "simple" code, e.g., by using GADTs. But to be honest, I mostly publish this so I have somewhere with thorough explanations that I can point people to when I make comments during code reviews.

The recommendations in this guide are split into three levels, based on the author's perception of their relative importance and applicability:

This document aspires to confirm to my Meta Style Guide.

Do you have ideas for this guide? Contact me!

Create Abstract Newtypes

Create many newtypes and hide their constructors. Where applicable, use the "smart constructor pattern".

  1. Example

    Instead of

    module Main where
    
    createUser ::
      -- | First name of user. Must not contain whitespace.
      Text ->
      Maybe User
    createUser nm =
      if textHasWhitespace nm
      then Nothing
      else Just (User nm)
    

    write

    module FirstName (FirstName, makeFirstName, getFirstName) where
    
    -- | First name, e.g., of a user.
    --
    -- Invariant: Does not contain whitespace. Constructor not exported to preserve
    -- invariant.
    newtype FirstName = FirstName { _getFirstName :: Text }
      deriving (Eq, Ord)
    
    makeFirstName :: Text -> Maybe FirstName
    makeFirstName nm =
      if textHasWhitespace nm
      then Nothing
      else Just (FirstName nm)
    
    -- Note: don't export _getFirstName directly, as record selectors can break the
    -- abstraction barrier we're establishing by not exporting the constructor.
    getFirstName :: FirstName -> Text
    getFirstName nm = _getFirstName
    
    module Main where
    
    import FirstName (FirstName)
    
    createUser ::
      -- | First name of user.
      FirstName ->
      User
    createUser nm = User nm  -- no need to check for whitespace!
    
  2. Justification

    A newtype provides semantic clarity---the name often hints at what the programmer intended to represent, rather than how they intended to represent it. In the example above, the FirstName newtype essentially obviates the comment on the parameter of createUser---it's already clear from the type what the parameter represents.

    A newtype with a hidden constructor and an exported "smart" constructor can maintain data structure invariants. Because these invariants are local to the module in which the newtype is defined, they're relatively easy to check. In the example above, it's easy to see that it's impossible to construct a FirstName containing whitespace outside of the module in which FirstName is defined. These invariants can also help avoid unnecessary partiality in other parts of the codebase, like how createUser no longer needs to check for whitespace, and so no longer needs to return a Maybe.

    The downside to this technique is the need to create additional modules and documentation. This amount of effort may not be appropriate in all circumstances.

  3. See Also

    https://haskell-at-work.com/episodes/2018-02-26-validation-with-smart-constructors.html

Write Explicit Export Lists

Every module should have an explicit export list.

  1. Example

    Instead of

    module Foo where
    

    write

    module Foo
      ( Type1(field1)
      , function1
      ) where
    
  2. Justification

    Using an explicit export list, a module may choose to not export certain functions. This leaves the module free to change their implementations or remove them entirely, with no concern that other modules (possibly even in other packages) might depend on them. Similarly, if a type's constructor is not exported, the representation of that type may be freely changed.

    A module with an explicit export list also has a clear API: it consists of the functions and types that have been exported. This makes it easier to reuse the functionality in the module.

Use toInteger

Use toInteger instead of fromIntegral and toEnum when possible.

  1. Example

    Instead of

    fromIntegral (0 :: Int) :: Integer
    

    or

    toEnum (0 :: Int) :: Integer
    

    write

    toInteger (0 :: Int)
    
  2. Justification

    fromIntegral and toEnum can throw exceptions:

    ghci> (fromIntegral :: Integer -> Natural) (-1)
    Exception: arithmetic underflow
    
    ghci> toEnum ((-1) :: Int) :: Natural
    Exception: toEnum: unexpected negative Int
    
    ghci> (toEnum :: Int -> Word) (-1)
    Exception: Enum.toEnum{Word}: tag (-1) is outside of bounds (0,18446744073709551615)
    

    fromIntegral can also cause surprising over- and under-flow:

    ghci> (fromIntegral :: Integer -> Word) (-1)
    18446744073709551615
    

    Finally, it is generally more clear to use less-polymorphic functions.

Nice to Have

Format to Minimize Diffs

Try to format code in a way that minimizes diffs. While this recommendation applies beyond Haskell, the Examples below include Haskell-specific tips.

  1. Example

    Instead of

    import LongModuleName (foo)
    import ShortName      (quux)
    

    write

    import LongModuleName (foo)
    import ShortName (quux)
    

    because if you import ReallyLongModuleName in a later comment, with the first formatting you have to fix the whitespace trailing LongModuleName and ShortModuleName.

    Use {} in patterns when you know you don't care about any fields of a constructor. Instead of

    case foo of
      Bar _ _ -> "bar"
      Quux _ -> "quux"
    

    write

    case foo of
      Bar {} -> "bar"
      Quux {} -> "quux"
    

    because you don't need to change these patterns if you add a field to Bar or Quux.

  2. Justification

    Minimizing diffs is helpful for focused code review, and when reviewing git history to understand a regression, see Programming Style Guide.

Minimal Polymorphism

Use less-polymorphic variants of functions where possible.

  1. Example

    Instead of

    fmap (+1) [1, 2, 3]
    

    write

    map (+1) [1, 2, 3]
    
  2. Justification

    The use of a polymorphic function sends a signal to the reader that the polymorphism was necessary. This is confusing when the polymorphism was, in fact, unnecessary. Furthermore, a less polymorphic function provides more information to the reader about the types of its arguments: in the expression fmap f x, the type of x could be any Functor, whereas in Data.Set.map f x, x must have type Set a for some a.