Skip to content

Instantly share code, notes, and snippets.

@laszlopandy
Last active January 23, 2019 06:47
Show Gist options
  • Save laszlopandy/c3bf56b6f87f71303c9f to your computer and use it in GitHub Desktop.
Save laszlopandy/c3bf56b6f87f71303c9f to your computer and use it in GitHub Desktop.

Elm Style Guide

Purpose

The goal of the style guide is foremost to promote consistency and reuse of pattern from other languages in order to improve readability and make Elm easier for beginners. This includes moving Elm away from Haskell’s indentation style and even making some parts look closer to JavaScript. These decisions are intentional.

We would like Elm to look friendly and familiar to users of any language — especially JavaScript — so they can discover Elm’s powerful features without being overwhelmed. This does not intend to weaken or discourage any features of Elm, but instead to make them more accessible.

A secondary goal of the style guide is to encourage short diffs when changes are made. This makes changes more clear, and helps when multiple people are collaborating.

Whitespace

Four-space indents

Elm code should use 4-space indents everywhere. This differs from other functional programming languages such as Haskell and OCaml, which use variable number of spaces depending on the syntactic construct (ie. let, if). Consistent 4-space indentation will make Elm code:

  • easier to use in editors without Elm support,
  • easier to follow visually,
  • less surprising for users of JavaScript.

It is important to use only spaces, not tabs. Usually the tabs vs. spaces debate is abstract and subjective. But in Elm, spaces are required to match the indentation level of the let keyboard when there are multiple variables:

calc box =
    let x = box.position.x
        y = box.position.y
    in
        x * y

If tabs are used and the file is viewed on GitHub where the tab width is different, then y = ... will not line up with x = ... on the line above.

Empty lines

  • There should be two empty lines between top-level definitions.
  • There should be at most one empty line inside a definition.

Partially empty lines

In some cases, the remainder of a line should be left empty to improve readability. The partially empty line helps to indicate the beginning or end of a record, list or function definition by visually dividing it from the rest of the code.

  • After = or -> when the right-hand side has multiple lines:
let width =
        Json.getProp "width" jsonValue
            |> Json.toInt
            |> Maybe.getDefault 0
  • After { or [ when starting a list or record, and after } or ] when closing one:
let urls = [
        "http://elm-lang.org",
        "http://example.com",
    ]
    imageProps = {
        url: "http://example.com/image.png",
        width: 100,
        height: 50,
    }

To see why this is necessary, consider this example:

{- incorrect style -}
let propUrl = Json.getProp "url" jsonValue
                |> Json.toString
                |> Maybe.getDefault ""

The second and third lines cannot be properly indented with a multiple of 4 spaces. |> ... can only be 2 or 6 spaces ahead of Json.getProp or 2 spaces behind it.

More importantly, if we later decide to change the name of propUrl we would have to re-indent all of following lines, creating a 3-line diff instead of a 1-line diff. As the code grows it becomes more tedious and confusing to re-indent all the lines.

 {- incorrect style -}
-let propUrl = Json.getProp "url" jsonValue
-                |> Json.toString
-                |> Maybe.getDefault ""
+let jsonPropUrl = Json.getProp "url" jsonValue
+                    |> Json.toString
+                    |> Maybe.getDefault ""

Syntax

let expressions

calc box =
    let first = box.position.x
        second = box.position.y
    in
        first * second
  • The first variable should be placed directly after let without a new line.
  • If there are multiple variables, all should be indented to match the start of the first variable.
  • in should exist on its own line, and the expression should follow on a new line which is indented to match the variables above.

if expressions

getName full =
    if full then "Evan Czaplicki" else "Evan"
getName full =
    if full
        then "Evan Czaplicki"
        else "Evan"
  • It is recommended to use multi-way if when multiple lines are needed, but if ... then ... else can be used on multiple lines in some cases (ie. here it is useful because Evan and Evan line up).
  • For the multiple line version then and else should be on their own lines and indented one forward from the if.
keyboardAction key n =
    if  | key == 40 -> n+1
        | key == 38 -> n-1
        | otherwise -> n
  • if should be followed by two spaces, a |, another space and the first condition.
  • The | should always line up with one above it.
keyboardDesc key =
    if  | key == 40 ->
            "You pressed the down arrow, which has key code " ++ show key
        | key == 38 ->
            "You pressed the up arrow, which has key code " ++ show key
        | otherwise ->
            "I don't know what you pressed"
  • If the expression after the -> is too long or has multiple lines, it should be moved onto a new line with another indent.

case expressions

keyboardDesc key =
    case key of
        40 ->
            "You pressed the down arrow, which has key code " ++ show key
        38 ->
            "You pressed the up arrow, which has key code " ++ show key
        otherwise ->
            "I don't know what you pressed"
  • There should always be a new line with an indent after case ... of so that if the variable name changes the following lines don't have to be re-indented.
  • The expression after -> should be on a new line if it is too long or consists of multiple lines (same as for multi-way if).

Lists

let urls = [
        "http://elm-lang.org",
        "http://example.com",
    ]
    numberNames = [ "one", "two", "three", "four" ]
    numbers = map show [
        1,
        2,
        3,
        4,
    ]
in
    [
        urls,
        numbers,
    ]
  • If the list fits on one line, there should be a space after the [, before the ] and after every ,.
  • If the list spans multiple lines, the line should be empty after [ and ] should be on its own non-indented line.
  • The [ can share a line with the = (or with a function name in case of function application), but not with the first item in the list.
  • If the list spans multiple lines, each item should be on its own line with an indent and a comma after it. Even the last line should have a comma so that when you add another element to the list you get a one-line diff instead of two-line. NOTE Elm's parser (as of 0.13) does not allow a comma after the last item so it is not possible to use this style yet.

Records

let urls = {
        elm = "http://elm-lang.org",
        example = "http://example.com",
    }
    numbers = { one = 1, two = 2, three = 3, four = 4 }
in
    { state |
        urls <- urls,
        numbers <- numbers,
    }
  • Same rules as lists.
  • There should be spaces on either side of the =.
  • When updating a record, the { ... | should stay together on the same line, and the property updates should be on new indented lines.

ADTs

data Kilometer = Kilometer Float
data Color
    = Red
    | Blue
    | Green
    | Rgb Int Int Int
  • If there is more than one constructor, all of them should be on a new line.
  • The = should be on the left of the first constructor and line up with the | below. This is the only exception where the = does not stay with the left-hand side and does not have white-space after it.

Function application

scene config state results (w, h) =
    toElement
        config.width
        h
        (searchWidgetElement
            config
            (Labels.labels config.language)
            state
            results)
  • If the list of arguments is too long, each argument should be put on its own line and indented once.
  • This style can be nested, as long as the code remains clear.
on  "keyup"
    getValue
    actions.handle
    UpdateSearchText
  • The exception to this rule is when the function name has 3 or less characters. In this case the first argument can be on the same line as the function and still line up with the left edge of the arguments below.

Operator application

stringToResult str =
    Json.parse str
        `Maybe.andThen` Json.getProp "items"
        `Maybe.andThen` Json.arrayMap itemJsonToEntry
        |> Maybe.getDefault []

longString =
    "The first part"
        ++ " continue on this line"
        ++ " and ends here"

widget config =
    scene
        <~ config 
        ~ state 
        ~ (searchResults config)
        ~ Window.dimensions
  • If the expression is too long to fit on one line, each operator should be on a new indented line.
  • Notice in Json.parse str that if operators are on a new line, the argument str should not. The function application style with arguments on new lines should not be mixed with operators on new lines. If the expressions between the operators are too long, a let variable should be used.

Real-world Example

David Biro's image search widget has been converted to use this style guide in all Elm code.

Anti-patterns

Using spaces to align names

imageJsonToEntry url imageJson = {
        url             = url,
        width           = getIntPropOrElse    0  "width"           imageJson,
        height          = getIntPropOrElse    0  "height"          imageJson,
        thumbnailUrl    = getStringPropOrElse "" "thumbnailLink"   imageJson,
        thumbnailWidth  = getIntPropOrElse    0  "thumbnailWidth"  imageJson,
        thumbnailHeight = getIntPropOrElse    0  "thumbnailHeight" imageJson
    }
  • This is not a good idea, because if the record is extended to include another, longer field name all the lines will have to be re-aligned.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment