This describes my Haskell code style and the rationale behind it. This article is not polished or finished at all, I add stuff as I come up with it.
-
One indentation level is 4 spaces. (I used to use 6, but the Haskell community seems to be mostly standardized to 4.)
main = do name <- getLine putStrLn ("Hello, " ++ name)
-
'where' placement is on its own line, intented half a level. This gives it a prominent place without consuming a full indentation level for itself.
-- 'where' centric: Small main function body using lots of 'where' definitions. -- This style keeps the indentation level of the sub-definitions low. where1 = definition where foo = <...> bar = <...> <...>
-
Literal tabs are syntax errors.
-
Whenever expressions are short and the current indentation level isn't too large, expressions continue on the same column as the start of the block.
main = do name <- getLine putStrLn ("Hello, " ++ name)
-
let
definitions are aligned with thein
body, which requires an additional space after thein
.main = let hw = "Hello, world!" in putStrLn hw
-
Whenever the positions of function arguments or operators like
->
or=
are in about the same column, they should be aligned.multiple = ... definitions = ... together = ... case x of Left l -> ... Right r -> ...
-
Operators should be grouped using spaces (or their absence).
collatz n | n <= 1 = [n] collatz n = let n' | even n = n `quot` 2 | otherwise = 3*n + 1 in n' : collatz n'
-
I use what I like to refer to as "sane Lisp style": use parentheses and general Lisp-style indentation, but don't leave out infix operators in the process. This makes a nice hybrid between the usual Lisp and the usual Haskell styles you see around.
when (a && b) (modifyTVar status (insert key value))
-
$
is reserved for functions that typically wrap large computations and appear near the top level, likeatomically
,lift
ortry
. Introducing another parenthesis for them seems unnecessary, as they mostly account for an expression's context, and now that it does by itself. In that sense$
is sort of a divider between infrastructure and program logic.
-
Name your unused patterns. An underscore is often hard to see, and naming it makes it explicit which part of the data you're not using.
case foo of Just (Left (_filename, handle)) -> doSomething handle _else -> ...
-
Specify imports block-wise, separated by a blank line, in several sections:
- Base
- Semi-standard (e.g. included in the Haskell Platform)
- Other
- Locally defined, i.e. part of the current project
This can sometimes be divided up some more, for example the Lens/Pipes ecosystems may get their own blocks.
-
Do not use type synonyms, unless you have a very good design reason to do so (example: lens types). If you have to, use them module-internally, but do not export them.
-
Types always have the form
name ::
with a single space, making it much easier to find the definition of something in a large source file, even without editor support. -
Long types can be broken into multiple lines that each start with an arrow:
liftM :: Monad m => (a -> b) -> m a -> m b
-
For some basic functions, prefer monomorphic implementations over overloaded ones whenever this is possible without loss of generality.
-- Mapping over lists map f [a,b,c] -- instead of fmap f [a,b,c] -- Chaining functions fmap . fmap -- instead of fmap fmap fmap -- Concatenating Strings "hello" ++ "world" -- instead of "hello" <> "world"
Note that there is only a handful of functions where I recommend doing this; the list above is almost exhaustive.
-
Advanced functions, like
traverse
, should always be used in their generalized form. It would just be a burden to the reader to keep all the locally definedtraverseTYPE
functions in mind, and there is no benefit for doing so. -
Import modules with names conflicting with basic modules, such as
Prelude
andData.List
, qualified; do this regardless of whether the conflicting basic modules are actually imported.import qualified Data.Foldable as F -- instead of import Prelude hiding (foldl, foldr, ...) import Data.Foldable