This document provides guidelines and advice on how to write and format code will fit with the existing code base and ensure some degree of consistency. Some of these rules are enforced by the automatic quality checker that is run after each push, but most are not. Not all of the current code follows these guidelines. If you find yourself working on such code, please reformat it while you are there. When something isn't covered by this guide you should stay consistent with the code in the other modules.
There are two sets of rules in this document. Style Rules, which low-level details such as the amount of indentation and how to name things, and Design Rules, which are less specific high-level guidelines on some of the principles underlying the compiler design.
Futhark uses Ormolu to enforce consistency in formatting style. The style is checked by our CI, so you should make sure any pull requests comply with Ormolus formatting before trying to merge.
Installing Ormolu can be done through Cabal, Stack, Nix or the Arch Linux package manager. For instance, to install through Cabal, run the following command:
cabal install ormolu
If you're running Nix or NixOS, you can just use nix-shell
to enter a
development environment with ormolu already installed.
The following command formats a single file:
ormolu -i FILE.hs
This command can be used to format all Haskell files in Futhark, while checking that the formatting is idempotent:
./tools/run-formatter.sh src unittests
The idempotence check is mostly done to make sure Ormolu (which is still a young tool in active development) doesn't introduce any unnecessary changes. Any idempotence errors should be reported upstream.
Emacs has ormolu.el to help with formatting. Once installed (as per the instructions on that page), it will automatically format any open Haskell file on save.
The Ormolu README lists further integrations for VS Code and vim.
Ormolu doesn't handle all aspects of coding style. For instance, it will do no significant rewrites of your code, like if-then-else to pattern matches or enforcing the 80 character line limit, but it will ensure consistency in alignment and basic formatting. Therefore, as a Futhark contributer, use Ormolu to ensure basic style consistency, while still taking care to follow the more general style rules listed below.
Maximum line length is 80 characters.
Ormolu doesn't enfore the 80 character line limit, so it is up to the user to introduce the necessary line breaks in the code. However, Ormolu will take a hint. Imagine you've got the following line of code:
onKernels :: (SegOp SegLevel KernelsMem -> ReuseAllocsM (SegOp SegLevel KernelsMem)) -> Stms KernelsMem -> ReuseAllocsM (Stms KernelsMem)
If the user introduces a newline before ReuseAllocsM
, turning the above into
the following:
onKernels :: (SegOp SegLevel KernelsMem -> ReuseAllocsM (SegOp SegLevel KernelsMem)) -> Stms KernelsMem ->
ReuseAllocsM (Stms KernelsMem)
Ormolu will pick up the hint and reformat the entire declaration to this:
onKernels ::
(SegOp SegLevel KernelsMem -> ReuseAllocsM (SegOp SegLevel KernelsMem)) ->
Stms KernelsMem ->
ReuseAllocsM (Stms KernelsMem)
In large do
-blocks, separate logically separate chunks of code with
a single blank line.
Long expressions should be split over multiple lines.
If splitting a definition using the $
operator, Ormolu will incrementally add
more indentation, which may sometimes be undesirable.
For instance, the following expression:
someAtrociouslyLongVariableName <- someFunction $ someOtherFunction withSomeVar $ someThirdFunction something $ map somethingElse
Will turn in to:
someAtrociouslyLongVariableName <-
someFunction $
someOtherFunction withSomeVar $
someThirdFunction something $
map somethingElse
If you'd rather keep everything equally nested, consider using the &
operator
instead, which is like a reverse $
. The code above is semantically identical
to this:
someAtrociouslyLongVariableName <-
map somethingElse
& someThirdFunction something
& someOtherFunction withSomeVar
& someFunction
Format export lists as follows:
module Data.Set
( -- * The @Set@ type
empty,
Set,
singleton,
-- * Querying
member,
) where
Generally, guards and pattern matches should be preferred over if-then-else clauses, where possible. E.g, instead of this:
foo b =
if not b then a
else c
Prefer;
foo False = a
foo True = c
Short cases should usually be put on a single line (when line length allows it).
Prefer pattern-matching in function clauses to case
. Consider using
view patterns,
but be careful not to go overboard.
Try to use explicit import lists or qualified
imports for standard
and third party libraries. This makes the code more robust against
changes in these libraries. Exception: the Prelude.
Write proper sentences; start with a capital letter and use proper punctuation.
Comment every top level function (particularly exported functions), and provide a type signature; use Haddock syntax in the comments. Comment every exported data type. Function example:
-- | Send a message on a socket. The socket must be in a connected
-- state. Returns the number of bytes sent. Applications are
-- responsible for ensuring that all data has been sent.
send :: Socket -- ^ Connected socket
-> ByteString -- ^ Data to send
-> IO Int -- ^ Bytes sent
For functions the documentation should give enough information to apply the function without looking at the function's definition.
Record example:
-- | Bla bla bla.
data Person = Person
{ age :: !Int -- ^ Age
, name :: !String -- ^ First name
}
For fields that require longer comments format them like so:
data Record = Record
{ -- | This is a very very very long comment that is split over
-- multiple lines.
field1 :: !Text
-- | This is a second very very very long comment that is split
-- over multiple lines.
, field2 :: !Int
}
Use camel case (e.g. functionName
) when naming functions and upper
camel case (e.g. DataType
) when naming data types. Use underscores
to separate words in variables and parameters. For compound names
consisting of just two words, it is acceptable to not separate them at
all, e.g. flatarrs
instead of flat_arrs
. If a variable or
parameter is also a function, use your judgement as to whether it is
most like a function or most like a value.
For readability reasons, don't capitalize all letters when using an
abbreviation. For example, write HttpServer
instead of
HTTPServer
. Exception: Two letter abbreviations, e.g. IO
.
Use concise and short names, but do not abbreviate aggresively,
especially in complex names. E.g, use filter_result_size
, not
flt_res_sz
.
Avoid partial functions like head
and !
. They can usually (always
in the case of head
) be replaced with a case
-expression that
provides a meaningful error message in the "impossible" case.
Do not use map
, mapM
, zipWithM
or similar with a nontrivial
anonymous function. Either give the function a name or use forM
.
Never use Literate Haskell.
Code should be compilable with -Wall -Werror
. There should be no
warnings. hlint
should not complain (except for a few rules that we
have disabled - see tools/style-check.sh
).
Never use braces and semicolons - always use whitespace-based layout instead (except for generated code).
When writing monadic code, use pure
instead of return
.
-
We try not to use too many crazy language extensions. Haskell is merely the implementation language, so we try to keep it simple, and we are not trying to push GHC to its limits. Syntactic language extensions are fine, as are most extensions that are isolated to the module and do not show up in the external module interface. Do not go overboard with type system trickery. The trickery we do have already causes plenty of pain.
-
Be aware of boolean blindness and try to avoid it. This helps significantly with avoiding partial functions. E.g, if a list must never be empty, represent it as a pair of an element and a list instead of just a list.
For long comments, we (try to) use the Notes convention from GHC, explained here. Essentially, instead of writing very long in-line comments that break the flow of the code, we write
-- See Note [Foo Bar]
and then somewhere else in the file (perhaps at the bottom), we put
-- Note [Foo Bar]
--
-- Here is how you foo the bar...
There is no automation around this or a hard rule for what a "long comment" is. It's just a convention.
Based on this style guide.