Coding Guidelines#

Note

The authoritative coding standards live in CLAUDE.md at the repository root. This page summarises the key rules for quick reference.

General Principles#

  • Focus on readability when writing code: readable code is easier to understand, thus easier to maintain, modify, and debug. Write code that reads from left-to-right and from top-to-bottom.

  • To improve maintainability (see “Building Maintainable Software” by Joost Visser):

    • write short units of code

    • write simple units of code

    • write code once

    • keep unit interfaces small

    • separate concerns in modules

    • automate tests

    • write clean code

    • write all possible scenarios of a case statement

Imports#

Use qualified imports for everything except:

  • Types (e.g. Entity (..), Key, Handler, DB)

  • Lens operators (e.g. (^.), (&), (.~), (?~))

  • Language extensions and pragmas

import ClassyPrelude
import ClassyPrelude.Yesod (Entity (..), Key)
import qualified ClassyPrelude.Yesod as Yesod
import Control.Lens ((^.), (&), (.~), (?~))
import qualified Data.Aeson as Aeson
import qualified Data.Text as Text

Models, Lenses, and Record Access#

Use lenses to get and set record fields. Do not use RecordDotPreprocessor or RecordWildCards; use lens operators instead.

When using lenses: prefer operators over functions (e.g. ^. over view), except when partially applying lens expressions (e.g. fmap (view userName)).

Use lenses inline — do not assign them to let or where bindings.

-- Good
langCode = domain ^. langCode

-- Bad (RecordDotPreprocessor)
langCode = domain.langCode

Naming#

Abbreviations#

variable name

meaning

e

entity

k

key

lc

lang code

prop

property

v

value

vac

vacancy

vf

vacancy filter

vfo

vacancy filter option

Prefixes#

prefix

meaning

m followed by an uppercase letter

this is a Maybe version of a value

unsafe followed by an uppercase letter

unsafe function, i.e. function might throw an exception

Postfixes#

postfix

meaning

E

persistent entity

K

persistent key

Variable Naming for Key and Entity Types#

Use the pattern {firstLetter}{TypeIndicator}:

type

variable name

Key User

uK

Key Vacancy

vK

Key Domain

dK

Entity User

uE

Entity Vacancy

vE

Entity Domain

dE

Prefer generic Key types over specific ID types (e.g. Key User instead of UserId), except in Persistent model definitions where the generated types are required.

Database Indices#

Use the naming scheme $table_$column_idx, e.g. an index for the version_id column on the component table must have the name component_version_id_idx.

Nested Case and If Expressions#

Avoid nested case and if expressions by defining additional functions, using monads and monad transformers.

Operators#

$

Do not use $, use parentheses.

<| / |> / .> / <.

Do not use Flow operators, use parentheses or >>= chains.

=<<

Do not use =<<, use >>=.

Style Preferences#

where over let

Use where clauses for local definitions instead of let expressions.

Minimal do syntax

Only use do when binds are not readable or when arguments are needed much later in the bind chain.

Prefer Complete Entities#

For consistency pass and receive complete entities instead of just entity keys or just entity values (if possible). Thus prefer:

optionInputValue :: Entity VacancyFilterOption -> Text
optionInputValue vfoE =
  toPathPiece (Yesod.entityKey vfoE)

over:

optionInputValue :: Key VacancyFilterOption -> Text
optionInputValue vfoK =
  toPathPiece vfoK