A demo of using GHCi as a persistent development environment. This example code uses a Yesod/Warp web server as a demo because it's simple to setup but a very common real-world use-case.
Associated blog post is here.
This project is runnable by simply loading DevelMain
in GHCi. You
can compile it with Cabal, but that's not the point of this demo.
The idea is that you have two main modules: Main
and
DevelMain
. The former for production, the latter for
development. DevelMain
starts your server and manages updates to the
running code.
Tip: remember to set flags if you want to avoid reloading all modules.
GHCi's job here is to run the warp process in a separate thread and
using the foreign-store
library, to retain a reference to the IORef
containing the
Application
(i.e. the web handler).
main =
do c <- newChan
app <- toWaiApp (Piggies c)
ref <- newIORef app
tid <- forkIO
(runSettings
defaultSettings { settingsPort = 1990 }
(\req -> do handler <- readIORef ref
handler req))
_ <- newStore c
_ <- newStore ref
return ()
When the user updates the IORef Application
, Warp will now server
the new code by virtue of the fact it reads from the IORef every time:
forkIO
(runSettings
defaultSettings { settingsPort = 1990 }
(\req -> do handler <- readIORef ref
handler req))
Emacs, or your-editor-of-choice, can have some keybinding for "update code". This will run the following in the GHCi process:
:l DevelMain.hs
If that results in an "OK" and not a compile error, it can then proceed to run
DevelMain.update
Which will auto-start the server, or, if already running, update the existing code running live. That update code is like this:
update =
do m <- lookupStore 1
case m of
Nothing -> main
Just store ->
do ref <- readStore store
c <- readStore (Store 0)
app <- toWaiApp (Piggies c)
writeIORef ref app
writeChan c ()
Emacs's haskell-mode already supports this, so you just need to setup a keybinding, e.g.
(define-key haskell-mode-map (kbd "<f12>") 'haskell-process-reload-devel-main)
An optional role for the browser is to auto-refresh the page, which is particularly handy if you are working on some visual aspect of the site and can't be bothered to switch to your browser, hit refresh and switch back. That is done by a trivial restart script:
$.get('/reload',function(){
window.location.reload();
});
And the route is:
getReloadR =
do reload <- fmap appReload getYesod
dupChan reload >>= readChan
This simply clones the channel (cloning in effect creates a broadcast to multiple readers, in this case; tabs in your browser) and then waits for a new event. As soon as a new event comes in the web handler returns and the script will refresh the page. Remember that earlier we read the channel from the foreign store and then write unit onto it:
c <- readStore (Store 0)
app <- toWaiApp (Piggies c)
writeIORef ref app
writeChan c ()
Sometimes your server will not be happy about having its things updated in-place and will probably segfault or give a funny GHC error. In these cases, it's safer just to kill the thread and restart the thread:
-- | Development version to be run inside GHCi.
module DevelMain where
import Application (getApplicationDev)
import Control.Exception (finally)
import Control.Concurrent
import Data.IORef
import Foreign.Store
import Network.Wai.Handler.Warp
-- | Start or restart the server.
update :: IO ()
update = do
mtidStore <- lookupStore tid_1
case mtidStore of
Nothing -> do
done <- newEmptyMVar
_done_0 <- newStore done
tid <- start done
tidRef <- newIORef tid
_tid_1 <- newStore tidRef
return ()
Just tidStore -> do
tidRef <- readStore tidStore
tid <- readIORef tidRef
done <- readStore (Store done_0)
killThread tid
takeMVar done
newTid <- start done
writeIORef tidRef newTid
where tid_1 = 1
done_0 = 0
-- | Start the server in a separate thread.
start :: MVar () -- ^ Written to when the thread is killed.
-> IO ThreadId
start done = do
(port,app) <- getApplicationDev
forkIO (finally (runSettings (setPort port defaultSettings) app)
(putMVar done ()))