-
Notifications
You must be signed in to change notification settings - Fork 43
Style guide
We use Fourmolu and EditorConfig to enforce style. Pull requests will be formatted automatically, but your editor should be configured to apply the formatters when a file is saved.
Lines should be no longer than 80 characters.
In exceptional circumstances, lines may be as long as 100 characters.
Rationale:
- 80 characters is short enough to display two files side-by-side on two screens.
- Research in human vision indicates that 45-75 characters long is the optimal range for lines of text. Every line break interrupts reading as they eyes search for the beginning of the next line. When the lines are too short, the interruptions are too frequent. When the lines are too long, the interruptions are less frequent but have longer duration. In either case, the reader expends more effort to read the same text.
- Source code is highly structured text. Long lines tend to obscure the structure by discouraging the use of the vertical dimension. Using vertical structure allows similar items to be spatially close.
Whitespace is not allowed at the end of lines.
Top-level pragmas immediately follow the corresponding definition.
id :: a -> a
id x = x
{-# INLINE id #-}
Sort import declarations alphabetically.
Do not separate import declarations into groups.
Rationale: Grouping import declarations interferes with tools that manage them automatically.
Prefer qualified imports for terms, except between closely related modules.
Prefer unqualified imports for types, except to disambiguate between similar names.
Use CamelCase for names, except in the test suite where prefixes (e.g. test_
) have a special meaning.
Names should be expressive and brief.
Rationale:
- Good names are no replacement for good documentation, but an expressive name can help recall the documentation.
- Names up to 10-12 characters long can be read with a single glance. Longer names take multiple eye movements to read.
Use full words instead of abbreviations, unless the abbreviation is much more common than the full word.
Rule of thumb: Write the abbreviation if you would say it (for example: HTTP, XML, JSON, HTML) but write the full word otherwise.
Don't capitalize all letters when using an abbreviation, except two-letter abbreviations which are the entire name (e.g. IO
).
For example, write HttpServer
instead of HTTPServer
,
Do not use prefixes on names to replace namespaces.
Instead, use a qualified import.
Do not use short names, like n
, sk
, and f
, unless they are so abstract that it is difficult to be more precise.
Use singular when naming modules e.g. use Data.Map
and Data.ByteString.Internal
instead of Data.Maps
and Data.ByteString.Internals
.
Put test cases and helpers for a module in a module with the corresponding name under the Test
module hierarchy, i.e. put tests for a module Data.Foo
in Test.Data.Foo
.
If data type has only one constructor then this data type name should be same as constructor name:
data User = User Int String
As above, the constructor of a newtype
should be named the same as the type.
Field name for newtype should start with either get
or un
followed by type name.
For wrappers with monadic semantics it should start with run
.
newtype Coin = Coin { getCoin :: Int }
Use the DuplicateRecordFields
extension to allow for duplicate field names and do not use unqualified field names.
data NetworkConfig = NetworkConfig
{ delay :: Microsecond
, port :: Word
}
f nc = delay nc -- not allowed
f NetworkConfig { delay } = delay -- NamedFieldPuns: better
-- verbose, but also good
f NetworkConfig { delay = specificName } = specificName
-- In case of ambiguity, to replace the prefixes below:
import {- not necessarily qualified -} SomeModule as NetworkConfig (NetworkConfig(..))
f nc = NetworkConfig.delay nc -- allowed: named import
Practice test-driven development: every change to the code should be preceded by a failing test.
Write a test case for every user-facing message.
Write complete sentences with correct capitalization and punctuation.
Write documentation for every top-level declaration (function, class, or type). Describe the fields and constructors of every data type. Write an explicit type signature for every top-level function and describe the arguments.
-- | 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.
-- | 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
}
Separate end-of-line comments from the code using 2 spaces.
data Parser = Parser
!Int -- Current position
!ByteString -- Remaining input
foo :: Int -> Int
foo n = salt * 32 + 9
where
salt = 453645243 -- Magic hash salt.
Use in-line links economically. You are encouraged to add links for API names. It is not necessary to add links for all API names in a Haddock comment. We therefore recommend adding a link to an API name if:
- The user might actually want to click on it for more information (in your judgment), and
- Only for the first occurrence of each API name in the comment (don't bother repeating a link)
By default, use the Strict
language feature.
Don't use lazy fields or bindings unless the benefit can be demonstrated with a profiling report.
Avoid over-using point-free style. For example, this is hard to read:
-- Bad:
f = (g .) . h
Do not mix sum and record types. Prefer using record types instead of raw product types. Do not make product types where there could be any reasonable ambiguity about what the arguments mean. Do not make product types where it’s not clear what the arguments mean, regardless of ambiguity.
-- Bad:
data Exists = Exists Sort Sort Variable Pattern
-- non-commutative operator, non-obvious operand order.
-- sum expression from lower_limit to upper_limit
data Sum = Sum Exp Exp Exp
-- non-commutative operator, non-obvious operand order,
-- non-obvious operand meaning (i.e. many people wouldn't think
-- about the d(exp) part as being part of the integral).
-- integrate expression from lower_limit to upper_limit d(expression)
data Integral = Integral Exp Exp Exp Exp
-- Acceptable:
data Exists = Exists PatternSort VariableSort Variable Pattern
-- commutative operator, ambiguity does not matter
data Add = Add Exp Exp
-- non-commutative operator, but really obvious operator order
-- that everyone knows.
data Div = Div Exp Exp
The package description lists many extensions that are enabled throughout the project.
The following language extensions may be enabled on a per-module basis:
AllowAmbiguousTypes
PolyKinds
TemplateHaskell
Use the class From
to define (total) conversions between types.
For example,
instance From String Text where
from = Text.pack
Instances should be homomorphisms preserving some structure of the input. If the preserved structure is not obvious, please document it.
Always use at least one type application or annotation with from
.
Rationale: The signature of from
is very generic and it is difficult to tell how it is being used when its type is inferred.
The following examples are all acceptable:
-- acceptable: both types are explicit and adjacent to 'from'
toText :: String -> Text
toText = from
-- acceptable: uses type application
someFunction =
let toText = from @String
in _
The history of a pull request should tell a reasonable story, rather than recording an exact history.
At the tip of the pull request, the build must succeed with the --pedantic
Stack option,
i.e. with the -Wall -Werror
GHC options.
All tests must pass at the tip of the pull request.
If a pull request introduces a bug that is later fixed in the same pull request,
the bug should be removed from history by squashing (using git rebase
) its resolution into its introduction.
The --fixup
option to git commit
is useful for automatically squashing bugs.
A bug may be preserved in the pull request history if it is especially tricky or demonstrates a problem in other code or tools;
in this case the resolution must be rebased to immediately follow the commit that introduced the bug.
Pull requests will be squashed and merged after approval.
Every pull request will be reviewed by another member before merging. The reviewer will use the review checklist in the pull request template to evaluate the pull request.