Skip to content

Compiler Style Guide

Ryan Hendrickson edited this page Jun 20, 2021 · 1 revision

This is a style guide for the Haskell code that comprises the PureScript compiler. The recommended style guide for PureScript code is available here.

This page is a work in progress; compiler maintainers should currently feel free to debate and edit this document until it stops being contentious, at which point we may migrate it into the repo proper.

General Principles

In rough order from most to least important—that is, you can read each point as if it were followed by ‘when this doesn't conflict with a previous point’.

  1. Code should be as readable as possible, assuming the reader is a proficient-but-not-legendary Haskell programmer.
  2. Code should minimize the number of edits needed to make changes.
  3. Code should be internally consistent—when practical, use the same approach to express similar ideas in nearby locations.
  4. Less code is better.

Specific Cases

Whitespace

Line breaks

Try to format comments to wrap around 80 characters, and find ways to break up lines of code that are longer than 120-ish characters. If readability isn't served by this guideline, ignore it.

Indentation

Generally: two spaces, no tabs. When indentation is used to align elements of a syntactic form that spans multiple lines, use the indentation in a way that doesn't require every line to change if the initial line changes (principle 2).

... when declaring data types

Show code

Choose one:

data Either a b = Left a | Right b
data Either a b
  = Left a
  | Right b

Do not write:

data Either a b = Left a
                | Right b

... when declaring type or kind signatures

Show code

Choose one:

foo :: forall a. ClassName a => Either Text a -> a -> a
foo
  :: forall a
  .  ClassName a
  => Either Text a
  -> a
  -> a

Do not write:

foo :: forall a
    .  ClassName a
    => Either Text a
    -> a
    -> a

... when declaring values or functions

Show code

Choose one:

foo a b = bar a . baz $ qux b
foo a b
  = bar a
  . baz
  $ qux b

Do not write:

foo a b = bar a
        . baz
        $ qux b

Intra-line alignment

Use sparingly?

Data types

Do use a custom data type instead of a Bool if at least one of the following applies:

  • the type is used in an exported member
  • a value of the type is used in a data structure which doesn't clarify the meaning of the value
  • it is easy to imagine a third option being added

Do declare a data type as a record if it has only one constructor and at least one of the following applies:

  • it is a simple newtype wrapper (prefix the accessor with one of the standard unwrapping words: get if the newtype is for instance selection (like using a particular Monoid), run if the newtype wraps a computation, un otherwise)
  • it has three or more arguments (prefix the accessor with an abbreviation of the type name)
  • it is expected to expand in the future (prefix the accessor with an abbreviation of the type name)

Do notation

Consider do notation if:

  • it reads best
  • it is used in adjacent branches

Avoid do notation if none of the above apply and:

  • no monadic work is being done
  • there is a simple way to express the computation directly with operators

Infix operators

Use infix notation for functions with the following names:

  • elem, notElem, member, notMember
  • cons, snoc
  • on
  • difference, intersect, union
  • isPrefixOf, isSuffixOf, generally anything named is*Of

Don't use infix notation for functions which would otherwise not merit it, just to avoid using flip: flip f x is equivalent to (`f` x), but the former is generally more readable. (???)

Pattern matching

Avoid catch-all patterns unless your logic is truly agnostic to new constructors being added to the type being matched.

Example Do write:
-- | Extract the value of a Foo.
maybeFooValue = \case
  Foo value -> Just value
  _ -> Nothing

Do not write:

-- | Extract a wibble, which is something that may or may not be present in any ctor.
findWibble = \case
  Foo wibble -> Just wibble
  Bar _ wibble -> Just wibble
  _ -> Nothing

When matching a data type, use the empty record pattern (CtorName{}) instead of wildcards (CtorName _ _) if the logic of the case would not change if new fields were added to that constructor.

Prefer lambda-case to multiple equational declarations, when possible. (This follows from principle 2; repeating the name of the function means more places to edit if that function is renamed.)

Clone this wiki locally