Principles:
- Maximize readability
- Code should be immediately readable by someone not familiar with that area. If you have to continuously refer to documentation, comments or dig into implementation to understand what's happening, the code isn't sufficiently self-descriptive and needs to be improved. When not possible, you must comment. Coding is a team sport, always write with other developers in mind
- Be aware of the context
- Use your judgement
There are some hard rules, but good style is context-sensitive. Something that is good in preferable in one place could be bad in another.
- Max 120 col line length
- Comment following Haddock style
Monadic sequences should normally go in one direction, including <-
from do notation
bad
result <- m >>= return
good
result <- return =<< m
That isn't to say we prever =<<
over >>=
good
result <-
action4
=<< action3
=<< action2
=<< action1
better
result <-
action1
>>= action2
>>= action3
>>= action4
good. This is easy to read left-to-right and scales as the lambda grows
m >>= (\x -> )
In general left bind does not scale well for inline code. This is ok.
(\x -> f x) =<< m
This is bad.
(\x -> do f x
g x
) =<< m
The lambda should be turned into a bound function.
Be careful of creating functions that have the same input type
f :: Int -> Int -> Int -> Int
This is a good time to look at using a record to name the arguments or to use newtypes around the Int
s. Note that the fact that the output is the same type as the input is not a concern: the below is fine
f :: Int -> Int
- comma-leading e.g.
data Student
= Student
{ firstName :: Text
, lastName :: Text
}
Something small can go on one line. The scalable way of declaring things that maintains vertical alignment properties is
data TransferTo
= TransferToTeacher (Entity Teacher)
| TransferToEmail Email
Signatures should lead with arrows
:: Esqueleto m expr backend
=> TeacherId
-> expr (Entity Student)
-> m ()
You should never need to align to preceding text. That is, your alignment point should always be 3rd, 5th, 7th etc. column (because we use two spaces of indenting).
e.g. Instead of
instance FromJSON (GameSession Never MathStandardAssignmentId) where
parseJSON = withObject "cannot parse GameSession" $ \o ->
GameSession <$> o .: "answers"
<*> o .: "domain-id"
<*> o .: "current-standard"
<*> o .: "sub-standard-perc"
<*> o .: "sub-sub-standard-perc"
<*> o .: "coins-gained"
<*> pure Never
which will cause a noisy diff if we rename GameSession
(each following line
also needs to be indented):
instance FromJSON (GameSession Never MathStandardAssignmentId) where
parseJSON = withObject "cannot parse GameSession" $ \o ->
- GameSession <$> o .: "answers"
- <*> o .: "domain-id"
- <*> o .: "current-standard"
- <*> o .: "sub-standard-perc"
- <*> o .: "sub-sub-standard-perc"
- <*> o .: "coins-gained"
- <*> pure Never
+ GameSession2 <$> o .: "answers"
+ <*> o .: "domain-id"
+ <*> o .: "current-standard"
+ <*> o .: "sub-standard-perc"
+ <*> o .: "sub-sub-standard-perc"
+ <*> o .: "coins-gained"
+ <*> pure Never
Instead, do
instance FromJSON (GameSession Never MathStandardAssignmentId) where
parseJSON =
withObject "cannot parse GameSession" $ \o ->
GameSession
<$> o .: "answers"
<*> o .: "domain-id"
<*> o .: "current-standard"
<*> o .: "sub-standard-perc"
<*> o .: "sub-sub-standard-perc"
<*> o .: "coins-gained"
<*> pure Never
instance FromJSON (GameSession Never MathStandardAssignmentId) where
parseJSON =
withObject "cannot parse GameSession" $ \o ->
- GameSession
+ GameSession2
<$> o .: "answers"
<*> o .: "domain-id"
<*> o .: "current-standard"
<*> o .: "sub-standard-perc"
<*> o .: "sub-sub-standard-perc"
<*> o .: "coins-gained"
<*> pure Never
The above is an example of operator-first style. Which we use generally, as in the following examples:
Foo
<$> o .: "this"
<*> o .: "that"
foo
<&> foo .~ bar
<.> baz .~ bat
fooBlahBlahHahaBlahBlahHaLongName
. barBlahBlahHahaBlahLongName
. bazBlahBlahHahaBlahHaLongName
$ bat quix
Nothing -> left
$ Text.pack "could not find parser for node \""
<> name
<> Text.pack "\" of type \""
<> typ
<> Text.pack "\" at "
<> file
<> Text.pack (": " ++ show n ++ ".")
logForwarderLambda :: Text -> Resource
logForwarderLambda envName = resource "LogForwarderLambda"
$ LambdaFunctionProperties
$ lambdaFunction
( lambdaFunctionCode
& lfcS3Bucket ?~ "frontrow-ops"
& lfcS3Key ?~ "logdna-lambda.zip"
)
"logdna_cloudwatch.lambda_handler"
(GetAtt "LambdaRole" "Arn")
(Literal Python27)
& lfFunctionName ?~ Literal (envName <> "-log-dna-forwarder")
& lfEnvironment ?~
( lambdaFunctionEnvironment
& lfeVariables ?~
[ ("LOGDNA_KEY", toJSON (Ref "LogDNAIngestionKey" :: Val Text))
]
)
This is consistent with comma-first style for structural expressions, and is generally easier to read in long functional expressions.
--- Bad
data SomeRecord = SomeRecord { someField :: Int
, someOtherField :: Double
} deriving (Eq, Show)
-- Good
data SomeRecord'
= SomeRecord'
{ someField :: Int
, someOtherField :: Double
} deriving (Eq, Show)
-- Bad
data SomeSum = FirstConstructor
| SecondConstructor Int
| ThirdConstructor Double Text
deriving (Eq, Show)
-- Good
data SomeSum'
= FirstConstructor'
| SecondConstructor' Int
| ThirdConstructor' Double Text
deriving (Eq, Show)
-- Bad - do is indented 2 spaces, so the expressions following it have to be indented 5 spaces
someBinding =
do x <- getLine
y <- getLine
putStrLn $ x <> y
-- Good - do is left hanging so the bindings are just indented 2 spaces
someBinding' = do
x <- getLine
y <- getLine
putStrLn $ x <> y
-- Bad - aligning to case pushes expressions way to the left, and aligning arrows is fiddly
someBinding mx = case mx of
Nothing -> 0
Just x -> x
-- Good - case on its own line, arrows don't need to be lined up
someBinding' mx =
case mx of
Nothing -> 0
Just x -> x
-- If your case-alternatives are more complex, you can put them on their own line:
someBinding'' mx =
case mx of
Nothing ->
putStrLn "Got nothin'"
Just x ->
putStrLn $ "Got " <> show x
-- Bad - aligning multiple let bindings like this is fiddly
someBinding mx =
let ma = fmap (+1) mx
mb = fmap (*2) mx
in (+) <$> ma <*> mb
-- Good - put `let` and `in` on its own line and then we can use normal spacing
someBinding''' mx =
let
ma = fmap (+1) mx
mb = fmap (*2) mx
in
(+) <$> ma <*> mb
-- Fine to keep on same line when you only have one binding, but use your judgement
someBinding'' mx =
let ma = fmap (+1) mx
in maybe 0 (*2) ma
Expressions with where
-bindings should place the body of the expression (if on
the next line) and the where
-bindings at the same (2-space) indentation, with
the where
keyword de-dented 1 space. Note that this causes an odd, 1-space
indentation of the where
keyword.
-- Bad - the `where` disappears
someBinding mx = f <$> mx <*> mx where f x y = x * x + y * y
-- Bad - the alignment is fiddly
someBinding' mx = g $ f <$> mx <*> mx
where f x y = x * x + y * y
g = maybe 0 (*2)
-- OK, but dissallowed in the interest of consistency
someBinding' mx = g $ f <$> mx <*> mx
where
f x y = x * x + y * y
g = maybe 0 (*2)
-- Good
someBinding mx =
f <$> mx <*> mx
where
f x y = x * x + y * y
-- Good - use the same indentation for non-multi-line expressions to avoid
-- having to change anything if/when they grow more lines
someBinding mx = f <$> mx <*> mx
where
f x y = x * x + y * y
-- Short lists and tuples can be placed on one line
names = ["Joe", "Bob", "Sam"]
car = ("Acura", "Integra", 2000)
-- Multiline lists and tuples and have commas first
names' =
[ "Joe"
, "Bob"
, "Same"
]
car' =
( "Acura"
, "Integra"
, 2000
)
-- You're more likely to see multiline records than tuples
teacher =
Teacher
{ firstName = "First"
, lastName = "Last"
, schoolId = 123
, hasPremium = True
}
Haskell's modules expose some variety in import style:
- Open imports
- Explicit imports
- Exclusionary imports
- Qualified imports
- Aliased imports
Good style prefers:
- Open imports for common libraries
base
mtl
- custom preludes
- Explicit imports for bringing lesser known functions in to scope
- Exclusionary imports for avoiding minor name clashes
lens
- Qualified imports for major name clashes
containers
unordered-containers
- Aliased imports for packaging and exporting many modules in a single module.
- creating a custom prelude
-- Good
import Control.Lens hiding (at)
import Control.Monad (forever)
import Control.Monad.Logger (logInfoN, logErrorN)
import Control.Monad.Trans.Reader
import Control.Monad.Trans.State
import qualified Data.Map as Map
import qualified Data.Text as Text
-- Bad
-- Overly open imports lead to increased ambiguity forcing common functions to be qualified.
import Control.Lens
import Control.Monad.Logger
import Control.Monad.Trans.Reader
import Control.Monad.Trans.State
import Data.Map as Map
import Data.Text as Text
-- Bad
-- Over qualification leads to increased line noise and length.
import qualified Control.Lens as Lens
import qualified Control.Monad.Logger as Logger
import qualified Control.Monad.Trans.Reader as Reader
import qualified Control.Monad.Trans.State as State
import qualified Data.Map as Map
import qualified Data.Text as Text
NOTE: if explicit imports exceed 80 columns, switch to a (sorted) list:
-- Bad
-- Long lines are hard to scan, inserting or removing an item is a noisier diff,
-- and it's difficult to sort a horizontal list
import System.IO (hPutStrLn, stderr, stdout, withFile, IOMode(..), hGetContents, hFlush, hClose)
-- Good
import System.IO
( IOMode(..)
, hClose
, hFlush
, hGetContents
, hPutStrLn
, stderr
, stdout
, withFile
)
It is also common to explicitly import types from a module and also import it qualified.
import Data.Map (Map)
import qualified Data.Map as Map
There are a number of common abbreviations that are used in the community to qualify imports.
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy.Char8 as BSL8
import qualified Data.List.NonEmpty as NE
import qualified Data.Sequence as Seq
Put one blank line between module
-where
and the start of your import
s. Put
your preferred prelude (when explicit) first, followed by a blank line, then the
rest of your imports.
The main import
group should be maintained by our stylish-haskell
configuration.
For vim users,
:stylish-haskell %
" or visually select the imports and
:'<,'>!stylish-haskell
An example of its results at the time of this writing is shown below, but what it actually does is less important than the fact that it's automated.
-- Bad
-- Improper spacing, improper sorting
module Foo
( bar
, baz
) where
import qualified Data.Map as Map
import TextAssets.S3
import Data.Text (Text)
import Unit
import Json
import Control.Lens
import qualified Data.Set as Set
import ClassyPrelude
import Network.AWS.S3
import Data.Set (Set)
import qualified Data.Text as T
import Network.AWS
import Data.Map (Map)
import Data.Conduit
-- Good
module Foo
( bar
, baz
) where
import ClassyPrelude
import Control.Lens
import Data.Conduit
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import Json (Json)
import qualified Json as J
import Network.AWS
import Network.AWS.S3
import TextAssets.S3
import Unit
Use sorted, multi-line exports. There are two exceptions to this rule:
-
A single-line export for
Main
, when it only exportsmain
:module Main (main) where
-
If the order of exports matters in your desired Haddock output, you may violate sorting to achieve it.
Otherwise:
-- Bad
module Driver (scienceOptions, socialStudiesOptions, mainWith) where
-- Good
module Driver
( mainWith
, scienceOptions
, socialStudiesOptions
) where
-
All Haskell packages MUST use the following
default-extensions
:default-extensions: - BangPatterns - DeriveAnyClass - DeriveFoldable - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - FlexibleContexts - FlexibleInstances - GADTs - GeneralizedNewtypeDeriving - LambdaCase - MultiParamTypeClasses - NoImplicitPrelude - NoMonomorphismRestriction - OverloadedStrings - RankNTypes - RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TypeApplications - TypeFamilies
NOTE:
NoImplicitPrelude
may be omitted in packages using the normal, implicitPrelude
everywhere.This defines a consistent, and minimally-extended Haskell environment. Other extensions MUST be defined via LANGUAGE pragmas in the modules where they're needed.
-
Place extensions on their own line, and sort them
-- Bad {-# LANGUAGE OverloadedStrings, RecordWildCards, DataKinds #-} -- Good {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}
-
Leave a blank line after the extensions list
{-# LANGAUGE OverloadedStrings #-} module Foo ( foo ) where
{-# LANGAUGE OverloadedStrings #-} -- | -- -- The Foo module does the foo-ing -- module Foo ( foo ) where