Skip to content

Instantly share code, notes, and snippets.

@evancz
Last active March 23, 2023 15:27
Show Gist options
  • Star 33 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save evancz/0a1f3717c92fe71702be to your computer and use it in GitHub Desktop.
Save evancz/0a1f3717c92fe71702be to your computer and use it in GitHub Desktop.
A style guide for Elm tools

Haskell Style Guide for Elm

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.

Line Length

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.

Variables

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.

Declarations

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.

Good

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.

Bad

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!

Types

Good

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])]

Bad

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.

Do Notation

Always prefer do notation to infix operators. Always!

Good

commaSpace :: Parser ()
commaSpace =
    do  string ","
        many (string " ")
        return ()

Bad

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.

@sjshuck
Copy link

sjshuck commented Mar 22, 2018

Furthermore, ending lines with a comma makes diffs messier because adding a field must change two lines instead of one.

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.

@Chesare22
Copy link

Is there any formatter that enforces this style?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment