This document describes the preferred code style for Haskell at Hornetsecurity. It is based on Simon Meier's Style Guide for Elevence.
If your're interested in an exciting career with Haskell have a look at our career page:
https://www.hornetsecurity.com/de/karriere
-
These rules are not set in stone. If you have good reason to break a rule, break it.
-
Optimize for readability and ease of reasoning about code. Code that is easy to read and understand offers fewer opportunities for bugs to hide.
-
Enlist the help of the compiler and type system. Good types make it harder to make mistakes.
-
Prefer code layout that scales to large numbers of functions and modules.
Stack is the preferred build tool. A template file for new projects is included in this repository.
Code should be compilable with -Wall -Werror
. There should be no
warnings. All unit tests, including hlint
, should pass.
The toplevel project directory should always contain
- the projects current
stack.yaml
, - the projects cabal file and
Setup.hs
, - a
README
file, preferably in Markdown syntax, - a
LICENSE
file, preferably plain text, - optionally older stack configuration files that are tested to work.
The projects library source resides in the src
directory, test code
within the test
directory.
- Control
mtl
(ortransformers
for simple stuff)
- Data Structures
bytestring
text
containers
unordered-containers
- Parsing
attoparsec
- Testing
tasty
hunit
(viatasty-hunit
)smallcheck
andQuickCheck
(viatasty-smallcheck
andtasty-quickcheck
)hlint
- Benchmarking
criterion
These formatting rules are implemented in style "Cramer" in the hindent tool.
Maximum line length is 80 characters. Lines may occasionally exceed 80 characters if wrapping earlier would be awkward.
The basic unit of indentation is 4 spaces. Certain construct may use half-indents of 2 spaces. Tabs are never allowed.
Use blank lines to aid readability.
One blank line between top-level definitions. No blank lines between type signatures and function definitions.
Surround binary operators with a single space on either side. Add no space inside parentheses, but do add space inside brackets and braces. Commas are followed by one space.
Don't insert a space after a lambda.
Align the constructors in a data type definition, fields in a record declaration, and elements in a list.
Separators (|
or ,
) are placed after a newline and followed by a
single space. They line up with the opening character (=
,
[
, or {
). The closing character stands on a line by its own.
Short enums and lists may be put on a single line.
Example:
data Either a b = Left a
| Right b
data Point = Point { x :: Int
, y :: Int
}
exceptions :: [StatusCode]
exceptions = [ InvalidStatusCode
, MissingContentHeader
, InternalServerError
]
Put pragmas immediately before the function/constructor/field they apply to. Example:
{-# INLINE id #-}
id :: a -> a
id x = x
You may or may not indent the code following a "hanging" lambda. Use your judgment. Some examples:
bar :: IO ()
bar =
forM_ [1, 2, 3] $ \n -> do
putStrLn "Here comes a number!"
print n
foo :: IO ()
foo =
alloca 10 $ \a ->
alloca 20 $ \b ->
cFunction a b
Format export lists as follows:
module Data.Set
( -- * The @Set@ type
Set
, empty
, singleton
-- * Querying
, member
) where
A single item export list may be written on one line:
module Main ( main ) where
The where
keyword appears on a line by itself and is indented by 2
spaces to set it apart from the rest of the code. The bindings in a
where
clause are indented an additional 2 spaces.
foldr f i = go
where
go [] = i
go (x:xs) = x `f` go xs
The bindings in a let
or let in
clause should be aligned and
directly follow the let
keyword. The in
keyword appears on a line
by itself, to set it apart from the rest of the code, with the
expression indented by 4 spaces.
let x = ...
y = ...
in
x + y
When the expression inside a let in
is a do
block, the do
keyword is put on the same line as the in
keyword to avoid overly
much vertical whitespace.
let x = ...
y = ...
in do
return (x + y)
Generally, guards and pattern matches should be preferred over
if then else
clauses, where possible. Short cases should usually be
put on a single line, as line length permits.
When writing non-monadic code (i.e. when not using do
), align the
if
, then
, and else
keywords:
foo = if ...
then ...
else ...
Otherwise, indent the then
and else
by one unit.
foo = do
someCode
if condition
then someMoreCode
else someAlternativeCode
Pattern in case
statements are indented with 4 spaces.
case foo x of
False -> return ()
True -> do
line <- getLine
process line
The do
keyword should be followed by a line break and the block's
statements indented by 4 spaces with respect to the previous line.
main = do
name <- getLine
putStrLn $ "Hello " ++ name ++ "!"
Imports should be sorted alphabetically and grouped by top-level module-hierarchy name. Align common keywords per import group and break explicit import lists as follows.
import Control.Lens ( preview, ix, at, traverseOf, toListOf
, view, use )
import qualified Control.Monad.Catch as Catch
Always prefer explicit import lists or qualified
imports. This makes the
code more robust against changes in the imported modules.
For qualified imports you should either use the full or abbreviated name of the last name(s) in the module hierarchy. Here are a few examples.
import qualified Control.Monad.Catch as Catch
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.Map as M
import qualified Data.HashMap as HM
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
Use camel case (e.g. functionName
) when naming functions and upper
camel case (e.g. DataType
) when naming data types.
For readability reasons, don't capitalize all letters when using an
abbreviation. For example, write HttpServer
instead of
HTTPServer
.
Avoid unprincipled abbreviations; in particular when naming top-level functions.
Prefix record fields either with the full name of the type or with the abbreviated name of the type. For example,
data EmailAddress = EmailAddress { eaName :: !Text
, eaDomain :: !Text
}
or
data EmailAddress = EmailAddress { emailAddressName :: !Text
, emailAddressDomain :: !Text
}
If you need to disambiguate constructors, then do this by post-fixing either the full or abbreviated name of the type. For example,
data ValidationError = ReferenceVE !Reference
| CharacterVE !Char
or
data ValidationError = ReferenceValidationError !Reference
| CharacterValidationError !Char
Use singular when naming modules e.g. use Data.Map
and
Data.ByteString.Internal
instead of Data.Maps
and
Data.ByteString.Internals
.
Avoid repeating a module's name in the name of the types and values it is defining. In particular avoid abbreviating the actual interesting part of the name in favor of repeating the module name. Modules form name spaces that should be made use of. For example,
-- Bad
module Foo.Bar where
data BarS = A | B
-- Good
module Foo.Bar where
data State = A | B
Write proper sentences; start with a capital letter and use proper punctuation.
Comment every top level declaration, particularly everything exported, and provide type signatures. Use Haddock syntax in the comments.
By default, use strict data types and lazy functions.
Constructor fields should be strict, unless there's an explicit reason to make them lazy. This avoids many common pitfalls caused by too much laziness and reduces the number of brain cycles the programmer has to spend thinking about evaluation order.
data Point = Point { pointX :: !Double -- ^ X coordinate
, pointY :: !Double -- ^ Y coordinate
}
Have function arguments be lazy unless you explicitly need them to be strict.
The most common case when you need strict function arguments is in recursion with an accumulator:
sum :: [Int] -> Int
sum = go 0
where
go !acc [] = acc
go acc (x:xs) = go (acc + x) xs