ghcid
is – at the current moment – the most important tool for Haskell development environments.
It is fast, reliable, works on all kinds of projects, and is remarkably versatile.
You can use it with any editor workflow, primarily by not integrating your editor! (though there are integrations available if you’re brave)
For these reasons, whenever someone asks about a Haskell IDE, I tell them to ignore the siren song of ghc-mod
, hdevtools
, intero
, haskell-ide-engine
, etc1, and just stick with the old faithful GHCi and ghcid
.
Use whatever editor you want – make sure it has syntax highlighting, and open up GHCi and/or ghcid
in a separate terminal.
Here are some things we’re going to do with it in this post:
As I think of additional “tricks” with ghcid
, I will be updating this post and adding them here.
If you have a suggestion or question, please open an issue on my blog’s GitHub :)
This is the bread and butter of what ghcid
is good for.
At this point, you’re probably used to running ghci
and doing :reload
to see whether or not your code compiles.
ghci
has some advantages over a cabal new-build
or stack build
or similar – it loads everything in interpreted byte code by default, which is much faster, and is capable of very intelligent module reloading to minimize work.
This can cut the feedback time from compilation dramatically.
By default, ghcid
will load with the flag -fno-code
enabled.
This turns off all code generation, and basically only gives you syntax and type checking.
When you eventually customize your ghcid
command, you will want to remember to either enable -fno-code
or -fobject-code
.
You need -fobject-code
in order to do stuff like run tests, check Template Haskell expressions, etc.
To customize your ghcid
command, you do this:
$ ghcid --command "the command to start ghci"
# example, for a Template Haskell heavy project:
$ ghcid --command "stack ghci package:lib --ghci-options=-fobject-code"
# example, to pick a single executable target:
$ ghcid --command "stack ghci package:exe:main-node"
# example, to defer type errors:
$ ghcid --command "stack ghci --ghci-options=-fdefer-type-errors"
At IOHK, I wrote up a Makefile
with the common ghcid
commands I use when working on the new wallet.
This command lets me say make ghcid
in the wallet-new
subdirectory and get lightning fast reloading of code, display of all warnings and errors, and lets me run through refactorings quite nice and quickly.
Sometimes GHC feels like a reluctant wizard. It knows things. You know it knows things. It knows that you know that it knows things. But it doesn’t want to tell you!
A common question that IDE authors want to ask is “What’s the type of this expression?”
ghc-mod
, intero
, all try to support this, to varying degrees of success and performance.
But GHC is curious and easily distracted, and would much rather tell you that you’re wrong than answer a question.
So let’s trick the wizard!
Just today, I was working on this snippet of code, pulled from my servant-persistent
starter pack/example project:
main :: IO ()
main = do
env <- lookupSetting "ENV" Development
port <- lookupSetting "PORT" 8081
logEnv <- defaultLogEnv
pool <- makePool env logEnv
store <- serverMetricStore <$> forkServer "localhost" 8000
waiMetrics <- registerWaiMetrics store
metr <- M.initializeWith store
let cfg = Config { configPool = pool
, configEnv = env
, configMetrics = metr
, configLogEnv = logEnv }
logger = setLogger env
runSqlPool doMigrations pool
generateJavaScript
run port $ logger $ metrics waiMetrics $ app cfg
I wanted to know what the type of the port
variable was.
With a more sophisticated toolchain, I might hover over port
, and get a tooltip telling me what.
But, we’re using the more primitive ghcid
.
Well, we know what it isn’t – It’s not ()
.
So, in the olden tradition, let’s loudly be wrong and await correction:
run (port :: ()) $ logger $ metrics waiMetrics $ app cfg
We fire up ghcid
, making sure to include the executable package target:
$ ghcid --command "stack ghci servant-persistent:exe:perservant"
And we’re greeted with an error message:
/home/matt/Projects/servant-persistent/app/Main.hs:37:10: error:
• Couldn't match type ‘()’ with ‘Int’
Expected type: warp-3.2.22:Network.Wai.Handler.Warp.Types.Port
Actual type: ()
• In the first argument of ‘run’, namely ‘(port :: ())’
In the expression: run (port :: ())
In a stmt of a 'do' block:
run (port :: ()) $ logger $ metrics waiMetrics $ app cfg
|
37 | run (port :: ()) $ logger $ metrics waiMetrics $ app cfg
| ^^^^^^^^^^
Ah, GHC expects it to be of type Int
. There we go!
This works well with functions, too.
Let’s say we want to know the type of run
, instead:
(run :: ()) port $ logger $ metrics waiMetrics $ app cfg
ghcid
is happy to tell us how wrong we are:
/home/matt/Projects/servant-persistent/app/Main.hs:37:5: error:
• Couldn't match expected type ‘Integer
-> Network.Wai.Application -> IO ()’
with actual type ‘()’
• The function ‘run :: ()’ is applied to one argument,
but its type ‘()’ has none
In the expression: (run :: ()) port
In a stmt of a 'do' block:
(run :: ()) port $ logger $ metrics waiMetrics $ app cfg
|
37 | (run :: ()) port $ logger $ metrics waiMetrics $ app cfg
| ^^^^^^^^^^^^^^^^
/home/matt/Projects/servant-persistent/app/Main.hs:37:6: error:
• Couldn't match expected type ‘()’
with actual type ‘warp-3.2.22:Network.Wai.Handler.Warp.Types.Port
-> Network.Wai.Application -> IO ()’
• Probable cause: ‘run’ is applied to too few arguments
In the expression: run :: ()
In the expression: (run :: ()) port
In a stmt of a 'do' block:
(run :: ()) port $ logger $ metrics waiMetrics $ app cfg
|
37 | (run :: ()) port $ logger $ metrics waiMetrics $ app cfg
| ^^^
Note that we get a slightly inconsistent message.
We’ve asserted that run :: ()
, and it has two expected types: one from definition, and one from inferred use.
The inferred type is Integer -> Application -> IO ()
.
The defined type is Port -> Application -> IO ()
.
It infers Integer
because, without run
forcing port
to be a Port
, it has nothing else to tell it what to be, and therefore defaults to Integer
.
ghcid
, in addition to the --command
flag, also takes a --test
flag.
The flag name is somewhat too specific – upon a successful compile with no warnings or errors, it will issue that command to GHCi for you.
It was initially intended for running tests, but we can do anything we like with it – and we are going to use it to get our web application reloading lightning fast.
This PR on the servant-persistent
project includes the necessary changes to get this running.
I have left a self-review on the PR, so I won’t explain too much here.
The ghcid
command we use is:
ghcid \
--command "stack ghci servant-persistent" \
--test "DevelMain.update"
This calls the DevelMain.update
function on every successful compile.
The DevelMain
module was mostly copied from the Yesod scaffold, with a few updates to make it work with this repo.
In truth, all you need to provide is a development-oriented function IO Application
that boots your application and gives you the WAI value.
The DevelMain code uses foreign-store
library to persist the state across GHCi sessions.
The ghcid
README links to an article on threepenny-gui apps with a similar strategy.
This is really fast – because GHCi can reload only exactly what it needs, and doesn’t have to link anything, you get to see your changes almost immediately.
What’s better than knowing your project compiles? Knowing that it passes the test suite!
We’ll use the --test
command here, and we specify that we want to run the tests.
In the cardano-sl
repository, I put a Makefile
command for test running:
ghcid-test: ## Have ghcid run the test suite for the wallet-new-specs on successful recompile
ghcid \
--command "stack ghci cardano-sl-wallet-new:lib cardano-sl-wallet-new:test:wallet-new-specs --ghci-options=-fobject-code" \
--test "main"
There’s a tricky bit here: We have to tell stack ghci
which package targets to load.
I specify the library (cardano-sl-wallet-new:lib
) so that it adds the library to the set of modules to watch for reloading.
Then I specify the test-suite I want to run (cardano-sl-wallet-new:test:wallet-new-specs
).
Finally I use --ghci-options=-fobject-code
, because this is fast, and I need to actually run the code (you get weird linker errors if you do -fno-code
and try to run the nonexistent code).
ghcid
is awesome and everyone owes Neil Mitchell a beverage.
If you have a suggested use case you want added, ping me on GitHub and I’ll credit you :)
These are great projects. But they are flaky, partially because GHC’s API is difficult to interface with, and partially because GHCi’s interactive features have some performance issues with larger code bases. For small projects and libraries, they often work great. For larger projects, or more varied environments, they show their pain. You can spend a lot of time fussing with the editor integration and waiting on some command to finish, or you can just develop habits that don’t need them (like ghcid
in a separate terminal). I say this as the author of the intero-neovim
plugin. ↩