Skip to content

Instantly share code, notes, and snippets.

@Chadtech
Last active March 4, 2018 20:17
Show Gist options
  • Save Chadtech/bc31ad66b5bc5987ad291adc929eecd0 to your computer and use it in GitHub Desktop.
Save Chadtech/bc31ad66b5bc5987ad291adc929eecd0 to your computer and use it in GitHub Desktop.

Chadtech Elm Style Guide

Im one of the few lucky enough to write Elm at work. I write Elm in the evening and on the week ends too, so I am kind of Elm 24/7 nowadays. Naturally Ive developed patterns and techniques to my code, that have been invaluable to maximizing my productivity as a web developer. I will share those techniques below.

But before that, let me say that Im obviously not the first person to write up an Elm style guide. There are other style guides out there, and I would say Im in at least 90% agreement with them. Just repeating what other people have already said about writing good Elm code wouldnt add much, so I have listed techniques that dont seem to be already widespread. Also, they are listed in order of how unusual they are, with the most unusual on top. I would love a discussion about good technique, and I would love to improve, so I invite you to criticize me on anything in this document.

Never use anonymous functions

Every time you write an anonymous function, you forgo writing a function with a name and its better for things to have names. Every time you write an anonymous function, you forgo writing a type signature the compiler can check. Type signatures are great, they help the compiler give you good error messages. Generally when writing anonymous functions, you increase the scope and indentation of your code, and its easier to work with code broken into smaller scopes and indentations.

-- Good
decoder : Decoder Place
decoder =
    Decode.int
        |> Decode.andThen toPlace


toPlace : Int -> Decoder Place
toPlace int =
    case int of
        1 ->
            Decode.succeed First

        2 ->
            Decode.succeed Second

        3 ->
            Decode.succeed Third

        _ ->
            Decode.fail "Invalid place integer"


-- Bad
decoder : Decoder Place
decoder =
    Decode.int
        |> Decode.andThen (\int ->
            case int of
                1 ->
                    Decode.succeed First

                2 ->
                    Decode.succeed Second

                3 ->
                    Decode.succeed Third

                _ ->
                    Decode.fail "Invalid place integer"
            )   

If you never use anonymous functions, you can ignore that entire part of the Elm syntax. Thats good, the fewer elements of syntax you have to know the better; it means it takes less mental machinery to do what you want to do. We dont want to be like JavaScript, where there are 4 or 5 elements of syntax that are substitutes, many of which you dont understand fully or recognize easily because they are sporadically and inconsistently used.

Let statements should mean something

In C syntax languages, like JavaScript, variable names that are capitalized mean something, even tho they arent technically a part of the syntax. When capitalized variables meaning something, developers are given license to assume the conclusion that the capitalized variable name represents a big module-y thing, and not just a value. You dont have to use capitalized variable names this way, but if you dont you miss out on a lot of code readability.

The same story could (or should) be told about let statements. If you only use them under exact conditions, then readers of your code can assume those exact conditions based off the presence of a let statement. If you dont use them under exact conditions, you miss out on readability.

I have my own ideas of when to use let statements, but I suppose having any exact conditions is much more important than the specific conditions I use. Here is what I do: only use let statements to declare a value that will be used in multiple places after the "in".

-- Good
reposition : Position -> Model -> Model
reposition position model =
    let
        adjustedPosition =
            adjust position
    in
    model
        |> updatePosition adjustedPosition
        |> newLine adjustedPosition


-- Bad
reposition : Position -> Model -> Model
reposition position model =
    model
        |> updatePosition (adjust position)
        |> newLine (adjust position)

You might be tempted to use a let statement as a short hand for a big complicated parameter in a simpler expression after the in. I certainly am. Instead, write a helper function.

-- Good
logout : Cmd Logout.Msg -> Model -> Model
logout logoutCmd model =
    (endSession model, logoutCmds logoutCmd)


logoutCmds : Cmd Logout.Msg -> Cmd Msg
logoutCmds logoutCmd =
    [ trackingCmd Tracking.Logout
    , Cmd.map LogoutMsg logoutCmd
    , Ports.closeApp
    ]
        |> Cmd.batch


-- Bad
logout : Cmd Logout.Msg -> Model -> Model
logout logoutCmd model =
    let
        cmd =
            [ trackingCmd Tracking.Logout
            , Cmd.map LogoutMsg logoutCmd
            , Ports.closeApp
            ]
                |> Cmd.batch
    in
    (endSession model, cmd)

Only nest your Model during refactoring, always start with a flat model

If you can keep your model flat, do it. Nested values are harder to extract, and harder to modify.

-- Good
type alias Model = 
    { x : Int
    , y : Int 
    , name : String
    }


setY : Int -> Model -> Model
setY newY model =
    { model | y = newY }


getY : Model -> Int
getY =
    .y


-- Bad
type alias Model = 
    { position : { x : Int, y : Int } 
    , name : String
    }


setY : Int -> Model -> Model
setY newY model =
    { model | position = setYInPosition newY model.position }


setYInPosition : Int -> Position -> Position
setYInPosition newY position =
    { position | y = newY }


getY : Model -> Int
getY model =
    model.position.y

As you can see, setYInPosition is a function we dont need without grouping x and y, and the function getY is lengthier. It doesnt sound like a big deal, but multiply by this extra stuff by 100 modules and you have a lot of unnecessary reading to do and a bulkier bundle to ship.

People seem to have to temptation to nest things into groups of logically related things, like x and y coordinates being grouped as a position. Thats an innocent temptation, but whats more important than the categorical tidiness of organizing stuff is the practical costs of grouping these values. Grouped values have to be deliberately ungrouped when used; an expense not associated with values that arent grouped to begin with.

Of course, sometimes you have to group things. Stuff like page models, or values that might not exist (Maybe { x : Int, y : Int}). Im not saying never group, but usually you figure out what groups make sense only after your software project has matured a bit, and its a lot easier to refactor a flat model into one with nested values, than refactoring nested values into differently-nested vales.

Name Msgs in the past tense

See here

Simplify complicated type signatures with records

Sometimes functions get big and complicated and have signatures like..

listItem : List (Attribute msg) -> Bool -> String -> Maybe String -> msg -> Html msg

Its not easy to figure out whats going there from a glance. But it is easy to figure out what these values are, because they have names:

type alias ListItemPayload msg =
    { attributes : List (Attribute msg)
    , selected : Bool
    , label : String
    , subLabel : Maybe String
    , clickMsg : msg
    }

listItem : ListItemPayload msg -> Html msg

More importantly, its not only the definition of the function thats clearer, but the invokation is clearer too.

-- Good
userListItem : User -> User -> Html msg
userListItem selectedUser user =
    { attributes = [ class [ UserItem ] ]
    , selected = selectedUser.id == user.id
    , label = user.name
    , subLabel = Nothing
    , clickMsg = UserClicked user.id
    }
        |> listItem


-- Bad
userListItem : User -> User -> Html msg
userListItem selectedUser user =
    listItem
        [ class [ UserItem ]
        (selectedUser.id == user.id)
        user.name
        Nothing
        (UserClicked user.id)

If listItem is a custom html function you use all over the place, then you generally wont be able to see the declaration of listItem at the same time you get to see it used (they will be in different modules that wont usually open on your screen simultaneously). If its a big project, you will inevitably forget what parameters this function takes, and what the values represent. Using a record type forces you to mention the name of the parameter whenever you want to use it, which means you can figure out whats going on just by reading any invokation of the function, not just the declaration.

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