Haskell Style Guide

    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 2 spaces.

    Always put a keyword on a new line.

    Line length

    The maximum preferred line length is 80 characters.

    Tip

    There is no hard rules when it comes to line length. Some lines just have to be a bit longer than usual. However, if your line of code exceeds this limit, try to split code into smaller chunks or break long lines over multiple shorter ones as much as you can.

    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.

    1. answers :: [Maybe Int]
    2. answers =
    3. [ Just 42
    4. , Just 7
    5. , Nothing
    6. ]

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

    1. -- + Good
    2. printQuestion
    3. :: Show a
    4. => Text -- ^ Question text
    5. -> [a] -- ^ List of available answers
    6. -> IO ()
    7. -- + Acceptable if function name is short
    8. fun :: Show a
    9. => Text -- ^ Question text
    10. -> [a] -- ^ List of available answers
    11. -> IO ()

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

    1. -- + Good
    2. data Foo = Foo
    3. { fooBar :: Bar
    4. , fooBaz :: Baz
    5. , fooQuux :: Quux
    6. } deriving (Eq, Show, Generic)
    7. deriving anyclass (FromJSON, ToJSON)
    8. -- + Acceptable
    9. data Foo =
    10. Foo { fooBar :: Bar
    11. , fooBaz :: Baz
    12. , fooQuux :: Quux
    13. } deriving (Eq, Show, Generic)
    14. deriving anyclass (FromJSON, ToJSON)

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

    1. -- + Good
    2. data TrafficLight
    3. = Red
    4. | Yellow
    5. | Green
    6. deriving (Eq, Ord, Enum, Bounded, Show, Read)
    7. -- + Acceptable
    8. data TrafficLight = Red
    9. | Yellow
    10. | Green
    11. deriving (Eq, Ord, Enum, Bounded, Show, Read)

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

    1. -- + Good
    2. createFoo = Foo
    3. <$> veryLongBar
    4. <*> veryLongBaz
    5. -- + Acceptable
    6. createFoo = Foo
    7. <$> veryLongBar
    8. <*> veryLongBaz
    9. -- + Acceptable
    10. createFoo =
    11. Foo <$> veryLongBar
    12. <*> veryLongBaz
    13. -- - Bad
    14. createFoo = Foo <$> veryLongBar
    15. <*> veryLongBaz
    16. -- - Bad
    17. createFoo =
    18. Foo -- there's no need to put the constructor on a separate line and have an extra line
    19. <$> veryLongBar
    20. <*> veryLongBaz

    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:

    1. veryLongProductionName
    2. firstArgumentOfThisFunction
    3. secondArgumentOfThisFunction
    4. (DummyDatatype withDummyField1 andDummyField2)
    5. lastArgumentOfThisFunction

    Naming

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

    Variant

    Use ids_with_underscores for local variables only.

    Try not to create new operators.

    1. -- What does this 'mouse operator' mean? :thinking_suicide:
    2. (~@@^>) :: Functor f => (a -> b) -> (a -> c -> d) -> (b -> f c) -> a -> f d

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

    1. -- + Good
    2. mapSelect :: forall a . (a -> Bool) -> (a -> a) -> (a -> a) -> [a] -> [a]
    3. mapSelect test ifTrue ifFalse = go
    4. where
    5. go :: [a] -> [a]
    6. go [] = []
    7. go (x:xs) =
    8. if test x
    9. then ifTrue x : go xs
    10. else ifFalse x : go xs
    11. -- - Bad
    12. mapSelect :: forall a . (a -> Bool) -> (a -> a) -> (a -> a) -> [a] -> [a]
    13. mapSelect p f g = go
    14. go :: [a] -> [a]
    15. go [] = []
    16. go (x:xs) =
    17. if p x
    18. then f x : go xs
    19. else g x : go xs

    Do not introduce unnecessarily long names for variables.

    1. -- + Good
    2. map :: (a -> b) -> [a] -> [b]
    3. map _ [] = []
    4. map f (x:xs) = f x : map f xs
    5. -- - Bad
    6. map :: (a -> b) -> [a] -> [b]
    7. map _ [] = []
    8. map function (firstElement:remainingList) =
    9. function firstElement : map function remainingList

    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 , String, Set Text, etc.).

    type aliases are allowed only for specializing general types:

    1. -- + Good
    2. data StateT s m a
    3. type State s = StateT s Identity
    4. -- - Bad
    5. type Size = Int

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

    1. data User = User Int String

    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.

    1. -- + Good
    2. data HealthReading = HealthReading
    3. { healthReadingDate :: UTCTime
    4. , healthReadingMeasurement :: Double
    5. }

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

    1. -- + Acceptable
    2. data HealthReading = HealthReading
    3. { hrDate :: UTCTime
    4. , hrMeasurement :: Double
    5. }

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

    1. newtype Measure = Measure
    2. { unMeasure :: Double -- ^ See how 2 spaces separate this comment
    3. }

    Write 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.

    1. -- | Single-line short comment.
    2. foo :: Int -> [a] -> [a]
    3. -- | Example of multi-line block comment which is very long
    4. -- and doesn't fit single line.
    5. foo :: Int -> [a] -> [a]
    6. -- + Good
    7. -- | 'replicate' @n x@ returns list of length @n@ with @x@ as the value of
    8. -- every element. This function is lazy in its returned value.
    9. replicate
    10. :: Int -- ^ Length of returned list
    11. -> a -- ^ Element to populate list
    12. -> [a]
    13. -- - Bad
    14. -- | 'replicate' @n x@ returns list of length @n@ with @x@ as the value of
    15. -- every element. This function is lazy in its returned value.
    16. replicate
    17. :: Int -- ^ Length of returned list
    18. {- | Element to populate list -}
    19. -> a
    20. -> [a]

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

    1. -- | The class of semigroups (types with an associative binary operation).
    2. --
    3. -- Instances should satisfy the associativity law:
    4. --
    5. -- * @x '<>' (y '<>' z) = (x '<>' y) '<>' z@
    6. class Semigroup a where
    7. (<>) :: a -> a -> a
    8. -- | The 'intersperse' function takes a character and places it
    9. -- between the characters of a 'Text'.
    10. --
    11. -- >>> T.intersperse '.' "SHIELD"
    12. -- "S.H.I.E.L.D"
    13. intersperse :: Char -> Text -> Text

    Guideline for module formatting

    Allowed tools for automatic module formatting:

    • : for formatting the import section and for alignment.

    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.

    1. {-# OPTIONS_GHC -fno-warn-orphans #-}
    2. {-# LANGUAGE ApplicativeDo #-}
    3. {-# LANGUAGE ScopedTypeVariables #-}
    4. {-# LANGUAGE TypeApplications #-}

    Always put language extensions in the relevant source file.

    Tip

    Language extensions must be listed at the very top of the file, above the module name.

    Export lists

    Use the following rules to format the export section:

    1. Always write an explicit export list.
    2. Indent the export list by 2 spaces.
    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.
    1. module Map
    2. ( -- * Data type
    3. Map
    4. , Key
    5. , empty
    6. -- * Update
    7. , insert
    8. , insertWith
    9. , alter
    10. ) where

    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. Imports from Hackage packages.
    2. Imports from the current project.

    Put a blank line between each group of imports.

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

    1. module MyProject.Foo
    2. ( Foo (..)
    3. ) where
    4. import Control.Exception (catch, try)
    5. import qualified Data.Aeson as Json
    6. import qualified Data.Text as Text
    7. import Data.Traversable (for)
    8. import MyProject.Ansi (errorMessage, infoMessage)
    9. import qualified MyProject.BigModule as Big
    10. data Foo
    11. ...

    Data declaration

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

    Records for data types with multiple constructors are forbidden.

    1. -- - Bad
    2. data Foo
    3. = Bar { bar1 :: Int, bar2 :: Double }
    4. | Baz { baz1 :: Int, baz2 :: Double, baz3 :: Text }
    5. -- + Good
    6. = FooBar Bar
    7. | FooBaz Baz
    8. data Bar = Bar { bar1 :: Int, bar2 :: Double }
    9. data Baz = Baz { baz1 :: Int, baz2 :: Double, baz3 :: Text }
    10. -- + Also good
    11. data Foo
    12. = Bar Int Double
    13. | Baz Int Double Text

    Strictness

    1. -- + Good
    2. data Settings = Settings
    3. { settingsHasTravis :: !Bool
    4. , settingsRetryCount :: !Int
    5. }
    6. -- - Bad
    7. data Settings = Settings
    8. { settingsHasTravis :: Bool
    9. , settingsConfigPath :: FilePath
    10. , settingsRetryCount :: Int
    11. }

    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:

    1. {-# LANGUAGE DeriveAnyClass #-}
    2. {-# LANGUAGE DerivingStrategies #-}
    3. {-# LANGUAGE GeneralizedNewtypeDeriving #-}
    4. newtype Id a = Id { unId :: Int }
    5. deriving stock (Generic)
    6. deriving newtype (Eq, Ord, Show, Hashable)
    7. deriving anyclass (FromJSON, ToJSON)

    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.

    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.

    1. sendEmail
    2. :: forall env m .
    3. ( MonadLog m
    4. , MonadEmail m
    5. , WithDb env m
    6. )
    7. => Email
    8. -> Subject
    9. -> Body
    10. -> Template
    11. -> m ()

    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.

    1. sendEmail
    2. toEmail
    3. subject@(Subject subj)
    4. body
    5. Template{..} -- default body variables
    6. = do
    7. <code goes here>

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

    Put operator fixity before operator signature:

    1. -- | Flipped version of '<$>'.
    2. infixl 1 <&>
    3. (<&>) :: Functor f => f a -> (a -> b) -> f b
    4. as <&> f = f <$> as

    Put pragmas immediately following the function they apply to.

    1. -- | Lifted version of 'T.putStrLn'.
    2. putTextLn :: MonadIO m => Text -> m ()
    3. putTextLn = liftIO . Text.putStrLn
    4. {-# INLINE putTextLn #-}
    5. {-# SPECIALIZE putTextLn :: Text -> IO () #-}

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

    1. data TypeRepMap (f :: k -> Type) = TypeRepMap
    2. { fingerprintAs :: {-# UNPACK #-} !(PrimArray Word64)
    3. , fingerprintBs :: {-# UNPACK #-} !(PrimArray Word64)
    4. , trAnys :: {-# UNPACK #-} !(Array Any)
    5. , trKeys :: {-# UNPACK #-} !(Array Any)
    6. }

    if-then-else clauses

    Prefer guards over if-then-else where possible.

    1. -- + Good
    2. showParity :: Int -> Bool
    3. showParity n
    4. | even n = "even"
    5. | otherwise = "odd"
    6. -- - Meh
    7. showParity :: Int -> Bool
    8. showParity n =
    9. if even n
    10. then "even"
    11. else "odd"

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

    1. shiftInts :: [Int] -> [Int]
    2. shiftInts = map $ \n -> if even n then n + 1 else n - 1

    Case expressions

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

    1. -- + Good
    2. firstOrDefault :: [a] -> a -> a
    3. firstOrDefault list def = case list of
    4. [] -> def
    5. x:_ -> x
    6. -- - Bad
    7. foo :: IO ()
    8. foo = getArgs >>= \case
    9. [] -> do
    10. putStrLn "No arguments provided"
    11. runWithNoArgs
    12. firstArg:secondArg:rest -> do
    13. putStrLn $ "The first argument is " ++ firstArg
    14. putStrLn $ "The second argument is " ++ secondArg
    15. _ -> pure ()

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

    1. fromMaybe :: a -> Maybe a -> a
    2. fromMaybe v = \case
    3. Nothing -> v
    4. Just x -> x

    let expressions

    Write every let-binding on a new line:

    1. isLimitedBy :: Integer -> Natural -> Bool
    2. isLimitedBy n limit =
    3. let intLimit = toInteger limit
    4. in n <= intLimit

    Put a let before each variable inside a do block.

    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:

    1. -- + Good
    2. foo :: Int -> a -> Int
    3. foo n x = length $ replicate n x
    4. -- - Bad
    5. foo :: Int -> a -> Int
    6. foo = (length . ) . replicate

    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.