Haskell Style Guide

Date
Tags haskell, programming
Target Audience Haskell programmers.
Epistemic Status Totally unsubstantiated opinions.

File layout

The dif­ferent ele­ments of a sample file have been iden­ti­fied be­low:

{-# LAN­GUAGE LambdaCase #-}                                                     -- (1)
{-# LAN­GUAGE TypeFam­ilies #-}

-- |                                                                            -- (2)
-- Module      : Test.Spec.Re­name
-- Copy­right   : (c) 2017 Mi­chael Walker
-- Li­cense     : MIT
-- Main­tainer  : Mi­chael Walker <mike@bar­ru­cadu.co.uk>
--
-- Func­tions for pro­jecting ex­pres­sions into a con­sistent namespace.
module Test.Spec.Re­name                                                         -- (3)
  ( -- * Pro­jec­tions
    pro­jec­tions
  , re­name
  , allRe­nam­ings
  -- ** The @These@ type
  , These(..)
  ) where

im­port           Con­trol.Arrow  (second)                                        -- (4)
im­port qual­i­fied Data.­Type­able  as T

im­port           Test.Spec.­Expr (Expr, en­vir­on­ment)
im­port           Test.Spec.­Type (raw­Ty­peRep)

-- | The @These@ type is like 'Either', but also has the case for when          -- (5)
-- we have both val­ues.
data These a b
  = This a
  | That b
  | These a b
  de­riving (Eq, Show)


------------------------------------------------------------------------------- -- (6)
-- Pro­jec­tions

-- | Find all type-­cor­rect ways of as­so­ci­ating en­vir­on­ment vari­ables.
pro­jec­tions :: Expr s1 m1 h1 -> Expr s2 m2 h2 -> [[(These String String, Ty­peRep)]]
pro­jec­tions e1 e2 = pro­jec­tionsFro­mEnv (env e1) (env e2) where
  env = map (second raw­Ty­peRep) . en­vir­on­ment
  1. Lan­guage prag­mas:
    • Al­pha­bet­ical order
    • One per line
    • Do not align the closing #-}s
  2. Module header:
    • Do not use the sta­bility or port­ab­ility fields
  3. Ex­port list:
    • One entry per line, with Had­dock­-­style head­ings (-- *, -- **, and so on) to di­vide it up
    • The list may be omitted for small or in­ternal mod­ules
  4. Im­ports:
    • Split into two groups: mod­ules from out­side the pro­ject and mod­ules from in­side the pro­ject
    • Add suf­fi­cient spa­cing between the im­port and the module name to in­clude the qual­i­fied keyword
    • Align im­port lists and as qual­i­fic­a­tions across both groups (even though there is a blank line)
    • If only im­porting in­stances, use this style: im­port Foo.In­stances ()
    • Put as many im­port specs on same line as pos­sible, wrap­ping at 80 char­ac­ters
    • Align im­port list on lines after the im­port under the start of the module name
    • There is no space between classes/­types and the list of its mem­bers: im­port Data.­Fold­able (Fold­able(­fold))
  5. Defin­i­tions:
    • One blank line between defin­i­tions
  6. Sep­ar­at­ors:
    • Defin­i­tions may be sep­ar­ated into named groups, where a group starts with two blank lines, a se­quence of 79 ’-’s, a title, and an­other single blank line
    • Con­sider mul­tiple mod­ules, if one file has many groups
    • Groups do not need to cor­res­pond ex­actly to sec­tions in the ex­port list: the ex­port list is for users of the mod­ule, the groups are for its de­velopers

Fur­ther­more, lines should use UNIX-­style (lf) line end­ings and have no trailing whitespace. The file should end with a single newline.

styl­ish-haskell should be used to format the lan­guage prag­mas, im­ports, and whitespace. The bullet points are just an Eng­lish-lan­guage de­scrip­tion of the ef­fect of this con­fig­ur­a­tion:

steps:
  - im­ports:
      align: global
      list_­a­lign: af­ter­_alias
      long_l­ist_­a­lign: in­line
      empty_l­ist_­a­lign: right_after
      list_pad­ding: mod­ule_­name
      sep­ar­ate_l­ists: false

  - lan­guage_­pragmas:
      style: ver­tical
      align: false
      re­move_re­dundant: true

  - trail­ing_whitespace: {}

columns: 80

newline: lf

In case of doubt, the tool takes pre­ced­ence.

Format­ting

One day I’d like to use a tool to format all my code for me (like gofmt or rustfmt), but:

Line length

Soft limit of 80 char­ac­ters, harder limit of 110 char­ac­ters, no ab­so­lute limit.

Lines longer than 80 char­ac­ters are ac­cept­able if breaking the line would in­tro­duce ugli­ness in some other way.

Align­ment

Sim­ilar items on ad­ja­cent lines may be sur­rounded with extra spaces to align them, if doing so would not in­tro­duce a large amount of extra spa­cing. Items on lines which are not ad­ja­cent, ig­noring lines con­sisting solely of whitespace or com­ments, should not be aligned.

Good:

in­stance NF­Data DPOR where
  rnf dpor = rnf ( dpor­Run­nable dpor
                 , dpor­Todo     dpor
                 , dpor­Done     dpor
                 , dporSleep    dpor
                 , dpor­Taken    dpor
                 , dpor­Ac­tion   dpor
                 )

Bad:

case rest of
  ((_, run­nable, _):_) -> map fst run­nable
  []                   -> []

Very bad:

case M.lookup tid' (dpor­Done dpor) of
  Just dpor' ->
    let done = M.in­sert tid' (grow state' tid' rest dpor') (dpor­Done dpor)
    in dpor { dpor­Done = done }
  Nothing    ->
    let taken = M.in­sert tid' a (dpor­Taken dpor)
    in dpor { dpor­Taken = if con­ser­vative then dpor­Taken dpor else taken }

In­dent­a­tion

Do not use tabs, in­dent with spaces. Use two spaces for each in­dent­a­tion level.

can­In­ter­rupt :: Dep­State -> ThreadId -> ThreadAc­tion -> Bool
can­In­ter­rupt dep­state tid act
  | is­Masked­In­ter­rupt­ible dep­state tid = case act of
    Blocked­Put­MVar  _ -> True
    BlockedRead­MVar _ -> True
    Blocked­TakeMVar _ -> True
    BlockedSTM      _ -> True
    Blocked­ThrowTo  _ -> True
    _ -> False
  | is­MaskedUn­in­ter­rupt­ible dep­state tid = False
  | oth­er­wise = True

autocheck­WayIO :: (Eq a, Show a) => Way -> Mem­Type -> ConcIO a -> IO Bool
autocheck­WayIO way mem­type concio =
  de­jafusWayIO way mem­type concio autocheck­Cases

Type sig­na­tures

If a type sig­na­ture is too long to fit on a single line, or you want to add com­ments to the in­di­vidual para­met­ers, it can be broken over mul­tiple lines.

In­dent­a­tion is with two spaces, and in­cludes the :: or => (for the first para­meter) and -> (for sub­sequent para­met­ers).

in­cor­por­at­eT­race
  :: Mem­Type
  -- ^ Memory model
  -> Bool
  -- ^ Whether the \"to-­do\" point which was used to create this new
  -- ex­e­cu­tion was con­ser­vative or not.
  -> Trace
  -- ^ The ex­e­cu­tion trace: the de­cision made, the run­nable threads,
  -- and the ac­tion per­formed.
  -> DPOR
  -> DPOR

Type­class con­straints should re­main on the same line as the func­tion name:

runSCT :: Mon­adRef r n
  => Way
  -- ^ How to run the con­cur­rent pro­gram.
  -> Mem­Type
  -- ^ The memory model to use for non-­syn­chron­ised @CRef@ op­er­a­tions.
  -> ConcT r n a
  -- ^ The com­pu­ta­tion to run many times.
  -> n [(Either Failure a, Trace)]

Data de­clar­a­tions

If there are mul­tiple con­struct­ors, break over lines like so:

data Way where
  Sys­tem­at­ic­ally :: Bounds -> Way
  Ran­domly :: Ran­domGen g => g -> Int -> Way

data Id­Source = Id
  { _nextCRId  :: Int
  , _next­MVId  :: Int
  , _nextTVId  :: Int
  , _nextTId   :: Int
  , _used­CR­Names :: [String]
  , _used­M­V­Names :: [String]
  , _usedTV­Names :: [String]
  , _usedTNames  :: [String]
  }
  de­riving (Eq, Ord, Show)

data ThreadAc­tion
  = Fork ThreadId
  | My­Th­readId
  | Get­Num­Cap­ab­il­ities Int
  | Set­Num­Cap­ab­il­ities Int
  | Yield
  de­riving (Eq, Show)

The de­riving clause comes on a new line with one level of in­dent­a­tion.

Type de­clar­a­tions

If the ori­ginal type is too long to fit on a single line, break it up just as you would a func­tion type sig­na­ture:

type Sched­uler state
  = [(De­cision, ThreadAc­tion)]
  -> Maybe (ThreadId, ThreadAc­tion)
  -> NonEmpty (ThreadId, Looka­head)
  -> state
  -> (Maybe ThreadId, state)

List and tuple de­clar­a­tions

Align the ele­ments in the list, with commas and brackets on the left:

fail­ures :: [Failure]
fail­ures =
  [ In­tern­alError
  , Abort
  , Dead­lock
  , STM­Dead­lock
  , Un­caugh­tEx­cep­tion
  , Il­leg­alSub­con­cur­rency
  ]

You may op­tion­ally avoid the newline, if it looks better in this case:

fail­ures :: [Failure]
fail­ures = [ In­tern­alError
           , Abort
           , Dead­lock
           , STM­Dead­lock
           , Un­caugh­tEx­cep­tion
           , Il­leg­alSub­con­cur­rency
           ]

Tuples larger than three ele­ments should be avoided. Some­times large tuples are useful though (for ex­ample, in writing NF­Data in­stances), in which case they are formatted like lists.

Case ex­pres­sions

Case ex­pres­sions may be in­dented in either of these ways:

(~=) :: Thread n r -> BlockedOn -> Bool
thread ~= theb­lock = case (_b­locking thread, theb­lock) of
  (Just (On­M­Var­Full  _), On­M­Var­Full  _) -> True
  (Just (On­M­Var­Empty _), On­M­Var­Empty _) -> True
  (Just (OnTVar      _), OnTVar      _) -> True
  (Just (On­Mask      _), On­Mask      _) -> True
  _ -> False

step­Throw t ts act e =
  case propagate (to­Ex­cep­tion e) t ts of
    Just ts' -> simple ts' act
    Nothing
      | t == ini­tial­Thread -> pure (Left Un­caugh­tEx­cep­tion, Single act)
      | oth­er­wise -> simple (kill t ts) act

Pragmas

Put pragmas between the type sig­na­ture and the defin­i­tion of the func­tion they apply to.

con­cat­Par­ti­tion :: (a -> Bool) -> [[a]] -> ([a], [a])
{-# IN­LINE con­cat­Par­ti­tion #-}
con­cat­Par­ti­tion p = foldl (foldr se­lect) ([], []) where
  se­lect a ~(ts, fs)
    | p a       = (a:ts, fs)
    | oth­er­wise = (ts, a:fs)

If the func­tion has no type sig­na­ture (it’s a local defin­i­tion in a where clause, for in­stance), put the pragma im­me­di­ately be­fore.

Hanging lambdas

Lines after a hanging lambda should be in­den­ted:

forkFi­nally :: Mon­ad­Conc m => m a -> (Either SomeEx­cep­tion a -> m ()) -> m (ThreadId m)
forkFi­nally ac­tion an­d_then =
  mask $ \re­store ->
    fork $ Ca.try (re­store ac­tion) >>= an­d_then

where clauses

If a func­tion con­tains a where clause, the where should be on the opening line of the func­tion, if short enough:

rep­res­ent­ative :: Eq a => Pre­dicate a -> Pre­dicate a
rep­res­ent­ative p xs = result { _fail­ures = choose . col­lect $ _fail­ures result } where
  result  = p xs
  col­lect = groupBy' [] ((==) `on` fst)
  choose  = map $ min­im­umBy (com­paring $ \(_, trc) -> (preEmps trc, length trc))

If the func­tion body in­cludes a linebreak, both it and the where-body should be in­dented by a fur­ther two spaces and the where put on a new line:

find­In­stance :: Expr s1 m1 h1 -> Expr s2 m2 h2 -> Maybe [(String, [String])]
find­In­stance eG eS
    | eS `isIn­stanceOf` eG = Just nameMap
    | oth­er­wise = Nothing
  where
    env = map fst . en­vir­on­ment'
    nameMap =
      map (\((s,g):sgs) -> (s, nub (g:map snd sgs))) .
      groupBy ((==) `on` fst) .
      sortOn fst $
      zip (env eS) (env eG)

Avoid nested where clauses and con­sider making mul­tiple top-­level defin­i­tions in­stead.

let ex­pres­sions

Mul­tiple lines in­side the let and in should be aligned:

grow state tid trc@((d, _, a):rest) dpor =
  let tid'   = tidOf tid d
      state' = up­dateDep­State state tid' a
  in case M.lookup tid' (dpor­Done dpor) of
       Just dpor' ->
         let done = M.in­sert tid' (grow state' tid' rest dpor') (dpor­Done dpor)
         in dpor { dpor­Done = done }
       Nothing ->
         let taken = M.in­sert tid' a (dpor­Taken dpor)
             sleep = dporSleep dpor `M.union` dpor­Taken dpor
             done  = M.in­sert tid' (sub­tree state' tid' sleep trc) (dpor­Done dpor)
         in dpor { dpor­Taken = if con­ser­vative then dpor­Taken dpor else taken
                 , dpor­Todo  = M.de­lete tid' (dpor­Todo dpor)
                 , dpor­Done  = done
                 }
grow _ _ [] _ = err "in­cor­por­at­eT­race" "trace ex­hausted without reading a to-do point!"

if-then-else ex­pres­sions

Guards and pat­tern matches should be pre­ferred over if-then-else ex­pres­sions gen­er­ally, use your judge­ment.

Out­side of do-nota­tion, if the en­tire ex­pres­sion does not fit on a single line, the then and else should begin in the same column as the if:

($$) :: Ord h => Expr s m h -> Expr s m h -> Maybe (Expr s m h)
f $$ e = case funTys (ex­pr­Ty­peRep f) of
    Just (fArgTy, fResTy) ->
      if fArgTy == ig­nore­Ty­peRep && is­Just (un­monad $ ex­pr­Ty­peRep e)
      then mkfun fResTy
      else mkfun =<< ex­pr­Ty­peRep f `fun­Res­ultTy` ex­pr­Ty­peRep e
    Nothing -> Nothing

In do-nota­tion the then and else should be in­dented an­other level:

check­File :: (Mon­adError FileError m, Mon­adIO m) => File­Config -> File­Path -> Int64 -> m ()
check­File fcfg fname bytes = do
  let max­bytes = max­SizeIn­Bytes fcfg
  if bytes > max­bytes
    then throw­Error (FileTooLarge bytes max­bytes)
    else do
      ok <- liftIO (ad­di­tion­al­Rules fcfg fname)
      case ok of
        Right _ -> pure ()
        Left  e -> throw­Error (FileDis­al­lowed e)

Naming

Use camel­Case for values and Pas­cal­Case for types and con­struct­ors.

Don’t cap­it­alise all let­ters when using an ab­bre­vi­ation: Ht­tpServer, not HT­TPServer. Two-­letter ac­ronyms are an ex­cep­tion to this.

For type vari­ables be con­sistent across type sig­na­tures, and con­sider a (short) name if there is one more mean­ingful than just a single let­ter.

check :: Mon­adSTM stm => Bool -> stm ()

throwSTM :: (Mon­adSTM stm, Ex­cep­tion e) => e -> stm a

catchSTM :: (Mon­adSTM stm, Ex­cep­tion e) => stm a -> (e -> stm a) -> stm a

lifte­dOrElse :: (Mon­adTransCon­trol t, Mon­adSTM stm)
  => (forall x. StT t x -> x)
  -> t stm a -> t stm a -> t stm a

Primes may be used to in­dicate that two values are re­lated. The number 0 may be used as a suffix to in­dicate that a value is “ini­tial” or “de­fault” in some way.

eval­u­ateDyn :: Monad m => Term s m -> [(String, Dy­namic s m)] -> Maybe (s -> Dy­namic s m)
eval­u­ateDyn e0 globals
    | all check (en­vir­on­ment e0) = Just (go [] e0)
    | oth­er­wise = Nothing
  where
    go _ (Lit _ _ dyn) _ = dyn
    go locals (Var _ var) _ = env locals var
    go locals (Let ty True _ b e) s =
      let mx = un­wrap­Mon­adicDyn (go locals b s)
      in un­safe­Wrap­Mon­adicDyn ty $ mx >>= \x -> un­wrap­Mon­adicDyn (go (x:locals) e s)
    go locals (Let _ False _ b e) s =
      let x = go locals b s
      in go (x:locals) e s
    go locals (Ap _ f e) s =
      let f' = go locals f s
          e' = go locals e s
      in f' `dynApp` (if ig­noreArg f' then ig­nore e' else e')
    go _ StateVar s = toDyn s

Num­bers greater than 0 may be used to in­dicate two values are sim­ilar but un­re­lated.

de­pendent :: Mem­Type -> Dep­State -> (ThreadId, ThreadAc­tion) -> (ThreadId, ThreadAc­tion) -> Bool
de­pendent mem­type ds (t1, a1) (t2, a2) = case re­wind a2 of
  Just l2
    | isSTM a1 && isSTM a2 ->
      not . S.null $ tvarsOf a1 `S.in­ter­sec­tion` tvarsOf a2
    | not (is­B­lock a1 && is­Bar­rier (sim­pli­fyLooka­head l2)) ->
      de­pend­ent' mem­type ds t1 a1 t2 l2
  _ -> de­pend­entAc­tions mem­type ds (sim­pli­fy­Ac­tion a1) (sim­pli­fy­Ac­tion a2)

Com­ments

Write cor­rectly punc­tu­ated and spelled Eng­lish sen­tences. Had­dock com­ments should have one space between the starting symbol and the text:

-- | This is good.

-- |This is bad.

Had­dock com­ments on type para­meters and data con­structors should use the -- ^ style (not the -- | style):

in­cor­por­at­eT­race
  :: Mem­Type
  -- ^ Memory model
  -> Bool
  -- ^ Whether the \"to-­do\" point which was used to create this new
  -- ex­e­cu­tion was con­ser­vative or not.
  -> Trace
  -- ^ The ex­e­cu­tion trace: the de­cision made, the run­nable threads,
  -- and the ac­tion per­formed.
  -> DPOR
  -> DPOR

-- | The sched­uler state
data DPORSched­State = DPORSched­State
  { sched­Sleep     :: Map ThreadId ThreadAc­tion
  -- ^ The sleep set: de­cisions not to make until some­thing de­pendent
  -- with them hap­pens.
  , sched­Prefix    :: [ThreadId]
  -- ^ De­cisions still to make
  , sched­B­Points   :: Seq (NonEmpty (ThreadId, Looka­head), [ThreadId])
  -- ^ Which threads are run­nable at each step, and the al­tern­ative
  -- de­cisions still to make.
  , schedIgnore    :: Bool
  -- ^ Whether to ig­nore this ex­e­cu­tion or not: @True@ if the
  -- ex­e­cu­tion is aborted due to all pos­sible de­cisions being in the
  -- sleep set, as then everything in this ex­e­cu­tion is covered by
  -- an­other.
  , sched­BoundKill :: Bool
  -- ^ Whether the ex­e­cu­tion was ter­min­ated due to all de­cisions being
  -- out of bounds.
  , sched­Dep­State  :: Dep­State
  -- ^ State used by the de­pend­ency func­tion to de­termine when to
  -- re­move de­cisions from the sleep set.
  }
  de­riving (Eq, Show)

Warn­ings and linting

Code should com­pile with -Wall with no warn­ings ex­cept for un­ne­ces­sary im­ports, if you sup­port mul­tiple ver­sions of lib­raries which have changed their API. Don’t in­tro­duce CPP simply to avoid warn­ings.

Use hlint to lint your code, this .h­lint.yaml con­fig­ur­a­tion file will work for ver­sion 2 and later:

# Module ex­port lists should gen­er­ally be pre­ferred, but may be
# omitted if the module is small or in­ternal.
- ig­nore: {name: Use module ex­port list}

# Re­cord pat­terns are just ugly.
- ig­nore: {name: Use re­cord pat­terns}

# Prefer ap­plic­ative op­er­ators over mon­adic ones.
- sug­gest: {name: Gen­er­alise mon­adic func­tions, lhs: re­turn, rhs: pure}

All lints should be fixed. However, some­times hlint is too zeal­ous, and there is a genuine reason why a lint cannot be fixed. Such cases may be added to the con­fig­ur­a­tion file with a com­ment ex­plaining why:

# GHC treats infix $ spe­cially wrt type check­ing, so that things like
# "runST $ do ..." work even though they're im­pre­dic­at­ive.
# Un­for­tu­nately, this means that HLint's "a­void lambda" warning for
# this module would lead to code which no longer com­piles!
- ig­nore: {name: Avoid lambda, within: Test.De­jaFu.­Conc}

Re­member the com­ment! You are cre­ating a tool­ing-ap­proved code smell!