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)
-
letdefinitions are aligned with theinbody, 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,liftortry. 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 definedtraverseTYPEfunctions in mind, and there is no benefit for doing so. -
Import modules with names conflicting with basic modules, such as
PreludeandData.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