Goal: a consistent style throughout all Elm projects that is easy to read and produces clean diffs to make debugging easier. This means valuing regularity and simplicity over cleverness.
Keep it under 80 characters. Going over is not the end of the world, but consider refactoring before you decide a line really must be longer.
Be Descriptive. One character abbreviations are rarely acceptable, especially not as arguments for top-level function declarations where you have no real context about what they are.
Be Specific. It may be possible to use fmap
instead of Map.map
or List.map
. It may be possible to use (>>=)
instead of List.concatMap
. Do not do this! Do not be clever. When reading through new code it is helpful to have extra hints about what kind of data you are working with. Being too generic leads to harder error messages and code that's harder to read.
Qualify variables. Always prefer qualified names. Set.union
is always preferable to union
. In large files and in large projects, it becomes very very difficult to figure out where variables came from without this.
Always have type annotations on top-level definitions.
Always bring the body of the declaration down a line.
Always have 2 empty lines between top-level declarations.
Never have multiple definitions of the same function as a substitute for a case
expression. Adding a variable becomes a change on tons of different lines and things get extremely messy when there are many possible patterns.
homeDirectory :: FilePath
homeDirectory =
"root" </> "files"
evaluate :: Boolean -> Bool
evaluate boolean =
case boolean of
Literal bool ->
bool
Not b ->
not (evaluate b)
And b b' ->
evaluate b && evaluate b'
Or b b' ->
evaluate b || evaluate b'
Now imagine one of the cases in evaluate
becomes drastically more complicated. Nothing needs to be reformatted so the diff will be minimal and the result will still look quite nice.
homeDirectory = "root" </> "files"
eval boolean = case boolean of
Literal bool -> bool
Not b -> not (eval b)
And b b' -> eval b && eval b'
Or b b' -> eval b || eval b'
eval (Literal bool) = bool
eval (Not b) = not (eval b)
eval (And b b') = eval b && eval b'
eval (Or b b') = eval b || eval b'
Imagine adding an argument to eval
in the second example. All lines need changing!
data Boolean
= Literal Bool
| Not Boolean
| And Boolean Boolean
| Or Boolean Boolean
deriving (Show)
data Circle = Circle
{ x :: Float
, y :: Float
, radius :: Float
}
deriving (Show, Eq)
type Graph =
[(Integer, [Integer])]
data Boolean = Literal Bool
| Not Boolean
| And Boolean Boolean
| Or Boolean Boolean
deriving (Show)
data Circle = Circle {
x :: Float,
y :: Float,
radius :: Float
} deriving (Show, Eq)
type Graph = [(Integer, [Integer])]
If the name Boolean
ever changes, indentation will change on all lines.
If we ever add a new field to Circle
that is longer than radius
we have to change the indentation of all lines, leading to a bad diff. Furthermore, ending lines with a comma makes diffs messier because adding a field must change two lines instead of one.
If we change the name of the type alias Graph
it'll be less clear if everything is on the same line. Furthermore, if the type being aliased ever becomes too long, it will need to move down a line anyway.
Always prefer do notation to infix operators. Always!
commaSpace :: Parser ()
commaSpace =
do string ","
many (string " ")
return ()
commaSpace :: Parser ()
commaSpace =
string "," >> many (string " ") >> return ()
Adding a new step creates a messy diff. Adding a couple extra steps means we need to start breaking things onto the next line, making an extremely messy diff. In that case you probably want to switch to do-notation anyway.
This is not true. If you add a field before
x
in the "Good"Circle
example, you also have to change two lines. It's a wash.