Don't panic.
(see also the Haskell Tool Stack
-
Haskell tooling is in a state of flux. There are lots of obvious warts, unfortunately, but the community is working on it. The below tools represent the current state of the art as of writing.
-
Emacs, Vim, and Atom have well-maintained and generally excellent Haskell integration.
-
stack: the package and build manager with Stackage integration.
-
hlint: a tool that checks for common style issues. You will be shocked at how comprehensive it is.
-
hdevtools: uses a client-server architecture to speed up syntax and type-checking in supported editors. Check it out!
-
ghcid: ghcid runs a little watch daemon in your console and can be useful as a sort of radar display on your second monitor. Spits out type errors and, if you want, runs your tests.
-
-Wall -Werror
. Turn it on. -
Promotional consideration: see Hao Lian's The always-updated treasure map to Haskell for a guide on which libraries to use.
MonadReader
is our friend. It takes this:
bakeCookies :: Flour -> Sugar -> Water -> Chips -> IO ()
bakeBrownies :: Flour -> Sugar -> Water -> Cocoa -> IO ()
bakeBread :: Flour -> Water -> Yeast -> IO ()
and turns it into this:
data Pantry = Pantry {
flour :: Flour
, sugar :: Sugar
, water :: Water
}
bakeCookies :: (MonadReader Pantry m, MonadIO m) => Chips -> m ()
bakeBrownies :: (MonadReader Pantry m, MonadIO m) => Cocoa -> m ()
bakeBread :: (MonadReader Pantry m, MonadIO m) => Yeast -> m ()
You can then choose how you want to discharge the MonadReader
constraint with either the MonadReader r (-> r)
instance or the
Monad m => MonadReader r (ReaderT r m)
. Here:
main =
let pantry = Pantry ... in
bakeCookies pantry
main =
let pantry = Pantry ... in
runReaderT bakeCookies pantry
We're also able to take advantage of the view
combinator in lens
, which lets us access the parts of the reader context without having to use the convoluted record syntax.
bakeCookies :: (MonadReader Pantry m, MonadIO m) => Chips -> m ()
bakeCookies = do
flour_ <- view flour
sugar_ <- view sugar
oxygenMolecule <- view (water . oxygenMolecule)
...
This works because the view
type is parametrized on MonadReader
. All along you've been doing this:
view _1 (1, 2) -- => 1
thinking you're just using function application. But what you've actually been doing is telling the typechecker to use the MonadReader r (-> r)
instance. For example:
runReaderT (view _1) (1, 2) -- => 1
is true also.
But more about lens
later.
The default Prelude has one big problem: You have to write out import Control.Monad
or import Data.List
every single time. Save yourself some typing. Medium Haskell projects almost always define their own prelude, and you should too. Turn on {-# LANGUAGE NoImplicitPrelude #-}
either at the file level or as a default extension in package.yaml
/project.cabal
.
A custom prelude is a good place to:
-
Import commonly used modules, like
Data.List
. -
Re-export someone else's prelude (
base-prelude
is a godsend). -
Re-export common monad transformers, since you'll be typing them every day.
-
Implement short utility functions specific to your project. For example, a
lens
Iso'
betweenUTCTime
andAmerica/Vancouver
time-zoneLocalTime
for a project that has to frequently handle PST/PDT times.
In practice, you want strictness on the outer spine of your data type.
data Record = Record { field1 :: !Int
, field2 :: !String
}
This is because
-
You probably don't need laziness in your data type.
-
Laziness comes at the price of accidentally building up big thunks that you have to hunt down when the deadline is nearing and everybody is mad at you.
-
GHC memory profiling, while not horrible, is still mildly arcane and somewhat of an art.
-
In the future (or possibly right now) you might compile with
-O -funbox-strict-fields
, which lays out your records without any indirection (pointers to thunks) at all and can improve performance.
In GHC 8, you can do this automatically with {-# LANGUAGE StrictData #-}
.
Monad transformers are even trickier to reason about, memory-wise. You will create programs with more intuitive memory dynamics if you use the strict version of StateT
and WriterT
:
import Control.Monad.Trans.State.Strict
import Control.Monad.Trans.Writer.Strict
import Control.Monad.Trans.RWS.Strict
-
Use
ExceptT
(inmtl
) instead ofEitherT
(in its own package). -
stack ghci
is your friend if you're working in one build target for a long period of time. Typing:r
to reload your code is much faster than running an incremental build. -
stack build --fast --ghc-options="+RTS -A256m -n2m -RTS"
is a good way of speeding up your builds. It tweaks values for the garbage collector so that the GHC processes running your code don't spend so much time in GC. It ... may or may not improve your performance. Builds are pretty slow in Haskell-land.