This style guide is derived from Johan Tibell's guide. It aims to make code easy to understand and uniform, while keeping diffs as small as possible.
We provide some configuration files for popular tools that help maintain code style:
stylish-haskell
confighlint
confighlint
config specific touniversum
, our custom Prelude- EditorConfig config
Note for Serokell people:
- All existing projects should continue using their current style guides, but may choose to switch to this one.
- All new projects must adhere to the guidelines below.
You should keep maximum line length below 80 characters. If necessary, you may use up to 100 characters, although this is discouraged.
Tabs are illegal. Use spaces for indenting. Indent your code blocks with 2
spaces. Indent the where
keyword with 2 spaces and the definitions within the
where
clause with 2 more spaces. Some examples:
sayHello :: IO ()
sayHello = do
name <- getLine
putStrLn $ greeting name
where
greeting name = "Hello, " ++ name ++ "!"
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
| p x = x : filter p xs
| otherwise = filter p xs
- You must add one blank line between top-level definitions.
- You must not add any blank lines between type signatures and function definitions.
- You should add one blank line between definitions in a type class instance
declaration or inside a
where
clause orlet
block if the definitions are large. - You may add blank lines inside a big
do
block to separate logical parts of it. - See below for usage of blank lines in the import section.
- You may align blocks of code with extra whitespace if alignment emphasizes
common structure.
data WalletApiRecord route = WalletApiRecord { _test :: route :- WTestApi -- /test , _wallets :: route :- WWalletsApi -- /wallets , _accounts :: route :- WAccountsApi -- /accounts , _addresses :: route :- WAddressesApi -- /addresses , _profile :: route :- WProfileApi -- /profile -- ... } deriving Generic
- You should surround binary operators with a single space on either side:
3 + 5
. You may choose not to do that to emphasize grouping of terms: `2- 2*2`.
- When using currying with binary operators, you must add one space between
the argument and the operation:
(42 +)
. - You must remove all trailing whitespace.
- You must append a trailing newline to all source files.
The last two points can be handled by EditorConfig. You are encouraged to install an EditorConfig plugin for your editor and use our config for Haskell files, but you may also configure your editor using specific instructions for removing trailing whitespace and appending a trailing newline.
You must use the following cases:
lowerCamelCase
for functions, variables, and global constants.UpperCamelCase
for types.
You should not use short names like n
, sk
, f
, unless their meaning is
clear from the context (function name, types, other variables, etc.).
You should not capitalize all letters in an abbreviation. For example, write
HttpServer
instead of HTTPServer
. Exception: two or three letter
abbreviations, e.g. IO
, STM
.
If data type has only one constructor then this data type name should be the
same as the constructor name (also applies to newtype
).
data User = User Int String
Field name for newtype
should start with un
or run
prefix followed by
type name (motivated by this
discussion):
run
for wrappers with monadic semanticun
for wrappers introduced for type safety
newtype Coin = Coin { unCoin :: Int }
newtype PureDHT a = PureDHT { runPureDHT :: State (Set NodeId) a }
Field names for record data type should start with every capital letter in type name.
data NetworkConfig = NetworkConfig
{ ncDelay :: Microsecond -- `nc` corresponds to `_N_etwork_C_onfig`
, ncPort :: Word
}
Add F
suffix to custom formatters to avoid name conflicts:
nodeF :: NodeId -> Builder
nodeF = build
If your comment is long enough to be put on a separate line, you should write proper sentences. Start with a capital letter and use proper punctuation. See below for end-of-line comments.
- You must provide a type signature for every top-level definition.
- You must comment every exported function and data type.
- You should comment every top-level function.
You must use Haddock syntax in the comments.
Example:
-- | Send a message on a socket. The socket must be in a connected
-- state. Returns the number of bytes sent. Applications are
-- responsible for ensuring that all data has been sent.
send
:: Socket -- ^ Connected socket
-> ByteString -- ^ Data to send
-> IO Int -- ^ Bytes sent
For functions, the documentation should give enough information to apply the function without looking at its definition.
Record example:
-- | Person representation used in address book.
data Person = Person
{ age :: Int -- ^ Age
, name :: String -- ^ First name
}
For fields that require longer comments, format them this way:
data Record = Record
{ -- | This is a very very very long comment that is split over
-- multiple lines.
field1 :: Text
-- | This is a second very very very long comment that is split
-- over multiple lines.
, field2 :: Int
}
Separate end-of-line comments from the code with 2 spaces. Align comments for data type definitions. Some examples:
data Parser = Parser
Int -- Current position
ByteString -- Remaining input
foo :: Int -> Int
foo n = salt * n + 9
where
salt = 453645243 -- Magic hash salt
Your documentation should include links to definitions, but use them sparingly. We recommend adding a link to an API name if:
- The user might actually want to click on it for more information (in your opinion), and
- Only for the first occurrence of each API name in the comment (do not bother repeating a link)
Write each LANGUAGE
pragma on its own line, sort them alphabetically and align
by max width among them. stylish-haskell
with our config
can do this for you.
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
Use singular when naming modules (e.g. use Data.Map
and
Data.ByteString.Internal
instead of Data.Maps
and
Data.ByteString.Internals
). Sometimes it is acceptable to use plural (e. g.
Types
, Instances
).
All modules must have explicit exports.
Format export lists as follows:
module Data.Set
( -- * The @Set@ type
Set
, empty
, singleton
-- * Querying
, member
) where
Some clarifications:
- Use 2-space indentation for export list.
- You may split export list into sections or just write everything in one section.
- You should sort each section alpabetically. However, within each section, classes, data types and type aliases should be written before functions.
- If your export list is empty, you may write in on the same line as the
module
declaration.
You may use weeder
to detect unused exports.
Imports should be grouped in the following order:
- Implicit import of custom prelude (for example
universum
) if you are using one. You may also usebase-noprelude
in order to avoid importing your custom prelude at all. - Everything from hackage packages or from your packages outside current project. "Project" is loosely defined as everything that is in your current repository.
- Everything from current project.
- Everything from current target (like
Bench.*
orTest.*
).
Put a blank line between each group of imports.
The imports in each group should be sorted alphabetically. stylish-haskell
with our config can do this for you.
You must use explicit import lists or qualified
imports.
Try to use qualified
imports only if import
list is big enough or there are conflicts in names. This makes the code more
robust against changes in these libraries. Exceptions:
- The Prelude or any custom prelude (e.g. Universum)
- Modules that only reexport stuff from other modules
Unqualified types (i.e. Map
vs. M.Map
) look pretty good and not so ugly.
Prefer two-line imports for such standard containers.
import Data.Map (Map)
import qualified Data.Map as Map
Align the constructors in a data type definition. Examples:
data Tree a
= Branch a (Tree a) (Tree a)
| Leaf
data HttpException
= InvalidStatusCode Int
| MissingContentHeader
Format records as follows:
data Person = Person
{ firstName :: String -- ^ First name
, lastName :: String -- ^ Last name
, age :: Int -- ^ Age
} deriving (Eq, Show)
You must not declare records with multiple constructors because their getters are partial functions.
As usual, separate type classes with ,
(comma and a space).
If you are using GHC-8.2.2 or higher you should use
-XDerivingStrategies
and specify the way you derive explicitly. Example:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
newtype SpecialId = SpecialId Int
deriving stock (Eq, Ord, Show, Generic)
deriving newtype Read
deriving anyclass (FromJSON, ToJSON)
All top-level functions must have type signatures.
All functions inside where
should have type signatures. Explicit type
signatures help avoid cryptic type errors. You may choose not to specify them
in cases like working with pure arithmetics where everything is Integer
, and
explicit type signatures look cumbersome.
You most likely need
-XExplicitForAll
and-XScopedTypeVariables
extensions to write polymorphic types of functions insidewhere
.
You should avoid overly general signatures for functions that are actually
used with only one type for each parameter. If you need the polymorhic version
(i.e. if you are instantiating it more than once or if you are writing a
library), you may use GHC's SPECIALIZE
pragma.
You should omit parentheses if you have only one constraint.
If function type signature is very long, you should place the type of each argument on its own line, and also align constraints similarly. Example:
putValueInState
:: (MonadIO m, WithLogger m)
=> UserState
-> Maybe Int
-> AppConfig
-> (Int -> m ())
-> m ()
If there are a lot of constraints, or they are very long, you may put them in
a separate type
definition, or format them as follows:
parseTree
:: ( KnownSpine components
, AllConstrained (ComponentTokenizer components) components
, AllConstrained (ComponentTokenToLit components) components
)
=> Text
-> Either (ParseError components) (Expr ParseTreeExt CommandId components)
If the line with argument names is too long, you should put each argument on a separate line with the usual 2-space indentation. and separate it somehow from body section.
putValueInState
userState
mValue@(Just x)
Config{..} -- { should go after ctor name without space
valueModificator
= do -- note how this line uses 4-space indentation
<code goes here>
In other cases place =
sign on the same line where function definition is.
You must put operator fixity before operator signature:
-- | Append a piece to the URI.
infixl 5 />
(/>) :: Uri -> PathPiece -> Uri
If you need to use pragmas, you must put them next to the function that they apply to. Example:
id :: a -> a
id x = x
{-# INLINE id #-}
For data type definitions, you must put the pragma before the type it applies to. Example:
data Array e = Array
{-# UNPACK #-} Int
ByteArray
Depending on the length of list elements, you should either keep the list on
one line or put each element on a separate line. In the latter case, put a
trailing ]
on a separate line, like }
when you format records. Example:
numbers = [1, 2, 4]
exceptions =
[ InvalidStatusCode
, MissingContentHeader
, InternalServerError
]
You must not insert a space after a lambda.
You may or may not indent the code following a "hanging" lambda. Use your judgement. 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
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
(when line length allows it).
When writing non-monadic code (i.e. when not using do
) where guards and
pattern matches cannot be used, you may align if-then-else
clauses like you
would normal expressions:
foo =
if ...
then ...
else ...
You may align if-then-else
in different style inside lambdas.
foo =
bar $ \qux -> if predicate qux
then doSomethingSilly
else someOtherCode
You may also write if-then-else
in imperative style inside do blocks:
foo = do
someCode
if condition
then do
someMoreCode
andMore
else -- you _may_ omit the `do` if the block is a one-liner
return ()
Use -XMultiwayIf
only if you need complex if-then-else
inside do
-blocks.
The alternatives in a case expression should be indented as follows:
foobar =
case something of
Just j -> foo
Nothing -> bar
You should align the ->
arrows whenever it helps readability.
It is suggested to use -XLambdaCase
. See this
discussion for some
usage examples.
Avoid over-using point-free style. For example, this is hard to read:
-- Bad:
f = (g .) . h
Modules and libraries should go in alphabetical order inside corresponding sections. You may put blank lines between groups in each section and sort each group independently.
You may use weeder
to detect
unused dependencies and exported modules.