Copyright | (c) 2016 Ertugrul Söylemez |
---|---|
License | BSD3 |
Maintainer | Markus Läll <markus.l2ll@gmail.com> |
Stability | experimental |
Safe Haskell | Safe-Inferred |
Language | Haskell2010 |
This module provides a rapid prototyping suite for GHCi that can be used standalone or integrated into editors. You can hot-reload individual running components as you make changes to their code. It is designed to shorten the development cycle during the development of long-running programs like servers, web applications and interactive user interfaces.
It can also be used in the context of batch-style programs: Keep resources that are expensive to create in memory and reuse them across module reloads instead of reloading/recomputing them after every code change.
Technically this package is a safe and convenient wrapper around foreign-store.
Read the "Safety and securty" section before using this module!
Synopsis
- data Rapid k
- rapid :: forall k r. Word32 -> (Rapid k -> IO r) -> IO r
- restart :: Ord k => Rapid k -> k -> IO () -> IO ()
- restartWith :: Ord k => (forall a. IO a -> IO (Async a)) -> Rapid k -> k -> IO () -> IO ()
- start :: Ord k => Rapid k -> k -> IO () -> IO ()
- startWith :: Ord k => (forall a. IO a -> IO (Async a)) -> Rapid k -> k -> IO () -> IO ()
- stop :: Ord k => Rapid k -> k -> x -> IO ()
- createRef :: (Ord k, Typeable a) => Rapid k -> k -> IO a -> IO a
- deleteRef :: Ord k => Rapid k -> k -> IO ()
- writeRef :: (Ord k, Typeable a) => Rapid k -> k -> IO a -> IO a
Introduction
While developing a project you may want to have your app running in the background and restart (parts of) it as you iterate. The premises to using this library are:
- you already have such a project
- you use GHCi
To use this functionality, create a new module in your project and
export the update
action:
module DevelMain (update) where import Rapid update :: IO () update = rapid 0 $ \r -> -- We'll list our components here shortly. pure ()
After loading this module in GHCi you run update
whenever you want
to restart the application in the background. E.g, in the case of a
web server that server is simply restarted on every update
:
import qualified Data.Text as T import Rapid import Snap.Core import Snap.Http.Server update = rapid 0 $ \r -> restart r "webserver" $ quickHttpServe (writeText (T.pack "Hello world!"))
The app keeps running in the background even when you reload modules,
and GHCi REPL continues to be functional as well. To apply new
changes, you simply reload DevelMain
again and run update
.
Changing "Hello world!" to something else above will start responding
with the new text after you run update
.
To stop the background thread, replace restart
with stop
within
update
and run it. Note that the action given to stop
is actually
ignored. It only takes the action argument for your convenience.
You can run multiple threads in the background simultaneously, have some of them restart while others not:
import MyProject.MyDatabase import MyProject.MyBackgroundWorker import MyProject.MyWebServer import Rapid update = rapid 0 $ \r -> do start r "database" myDatabase -- doesn't restart on update start r "worker" myBackgroundWorker -- doesn't restart on update restart r "webserver" myWebServer -- restarts on update
Usually you'd use restart
in front of the component you are working
on, while using start
for others.
Note that even while working on MyProject.MyWebServer
you're always
reloading DevelMain
to get the new update
.
Communication
If you need your background threads to communicate with each other, for
example by using concurrency primitives, some additional support is
required. You cannot just create a TVar
within your update
action.
It would be a different one for every invocation, so threads that are
restarted would not communicate with already running threads, because
they would use a fresh TVar
, while the old threads would still use the
old one.
To solve this, you need to wrap your newTVar
action with createRef
.
The TVar
created this way will survive reloads in the same way as
background threads do. In particular, if there is already one from an
older invocation of update
, it will be reused:
import Control.Concurrent.STM import Control.Monad import Rapid update = rapid 0 $ \r -> do mv1 <- createRef r "var1" newEmptyTMVarIO mv2 <- createRef r "var2" newEmptyTMVarIO start r "producer" $ mapM_ (atomically . putTMVar mv1) [0 :: Integer ..] restart r "consumer" $ forever . atomically $ do x <- takeTMVar mv1 putTMVar mv2 (x, "blah") -- For debugging the update action: replicateM_ 3 $ atomically (takeTMVar mv2) >>= print
You can now change the string "blah"
in the consumer thread and then
run update
. You will notice that the numbers in the left component of
the tuples keep increasing even after a reload, while the string in the
right component changes. That means the producer thread was not
restarted, but the consumer thread was. Yet the restarted consumer
thread still refers to the same TVar
as before, so it still receives
from the producer.
Reusing expensive resources
This library can also be used to shorten the development cycle when using expensive resources:
import Control.Exception import Data.Aeson import qualified Data.ByteString as B update = rapid 0 $ \r -> value <- createRef r "file" $ B.readFile "blah.json" >>= either (throwIO . userError) pure . eitherDecode -- You can now reuse 'value' across reloads.
The above parses blah.json just once on startup. To actually recreate
the value replace createRef
to writeRef
temporarily and run update
.
Using deleteRef
in the same manner removes values you no longer need.
Cabal notes
In general a Cabal project should not have this library as a build-time
dependency. However, in certain environments (like Nix-based
development) it may be beneficial to include it in the .cabal
file
regardless. A simple solution is to add a flag:
flag Devel default: False description: Enable development dependencies manual: True library build-depends: base >= 4.8 && < 5, {- ... -} if flag(devel) build-depends: rapid {- ... -}
Now you can configure your project with -fdevel
during development and
have this module available.
Emacs integration
This library integrates well with
haskell-interactive-mode,
particularly with its somewhat hidden
haskell-process-reload-devel-main
function.
This function finds your DevelMain
module by looking for a buffer
named DevelMain.hs
, loads or reloads it in your current project's
interactive session and then runs update
. Assuming that you are
already using haskell-interactive-mode all you need to do to use it is
to keep your DevelMain
module open in a buffer and type M-x
haskell-process-reload-devel-main RET
when you want to hot-reload. You
may want to bind it to a key:
(define-key haskell-mode-map (kbd "C-c m") 'haskell-process-reload-devel-main)
Since you will likely always reload the current module before running
update
, you can save a few keystrokes by defining a small function
that does both and bind that one to a key instead:
(defun my-haskell-run-devel () "Reloads the current module and then hot-reloads code via DevelMain.update." (interactive) (haskell-process-load-file) (haskell-process-reload-devel-main)) (define-key haskell-mode-map (kbd "C-c m") 'my-haskell-run-devel)
Safety and security
It's easy to crash GHCi with this library. In order to prevent that, follow these rules:
- Do not change your service name type (the second argument to
start
,stop
andrestart
) within a session. Simplest way to do that is to resist the temptation to define a custom name type and just use strings instead. If you do change the name type then you need to restart GHCi. - Be careful with mutable variables created with
createRef
: if the value type changes (e.g. constructors or fields were changed), so must the variable be recreated, e.g by usingwriteRef
once. This likely also entails restarting all the threads that were using this variable. Again, the safest option is to restart GHCi. - If any package in the current environment changes (especially this
library itself), for example by updating a package via
cabal
orstack
, theupdate
action is likely to crash or go wrong in subtle ways due to binary incompatibility. Again, restarting GHCi solves this. - __This library is a development tool! Do not use it to hot-reload productive environments!__ There are much safer and more appropriate ways to hot-reload code in production, for example by using a plugin system.
The reason for this unsafety is that the underlying foreign-store library is itself unsafe by nature, requiring us to maintain binary compatibility. This library hides most of that unsafety, but still requires you to follow the rules listed above.
Hot code reloading
:: forall k r. Word32 | Store index (if in doubt, use 0). |
-> (Rapid k -> IO r) | Action on the Rapid state. |
-> IO r |
Retrieve the current Rapid state handle, and pass it to the given
continuation. If the state handle doesn't exist, it is created. The
key type k
is used for naming reloadable services like threads.
Warning: The key type must not change during a session. If you need to change the key type, currently the safest option is to restart GHCi.
This function uses the foreign-store library to establish a state handle that survives GHCi reloads and is suitable for hot reloading.
The first argument is the Store
index. If you do not use the
foreign-store library in your development workflow, just use 0,
otherwise use any unused index.
Threads
:: Ord k | |
=> Rapid k | Rapid state handle. |
-> k | Name of the thread. |
-> IO () | Action the thread runs. |
-> IO () |
Create a thread with the given name that runs the given action.
The thread is restarted each time an update occurs.
:: Ord k | |
=> (forall a. IO a -> IO (Async a)) | Thread creation function. |
-> Rapid k | Rapid state handle. |
-> k | Name of the thread. |
-> IO () | Action the thread runs. |
-> IO () |
Create a thread with the given name that runs the given action.
The thread is restarted each time an update occurs.
The first argument is the function used to create the thread. It can
be used to select between async
, asyncBound
and asyncOn
.
:: Ord k | |
=> Rapid k | Rapid state handle. |
-> k | Name of the thread. |
-> IO () | Action the thread runs. |
-> IO () |
Create a thread with the given name that runs the given action.
When an update occurs and the thread is currently not running, it is started.
:: Ord k | |
=> (forall a. IO a -> IO (Async a)) | Thread creation function. |
-> Rapid k | Rapid state handle. |
-> k | Name of the thread. |
-> IO () | Action the thread runs. |
-> IO () |
Create a thread with the given name that runs the given action.
When an update occurs and the thread is currently not running, it is started.
The first argument is the function used to create the thread. It can
be used to select between async
, asyncBound
and asyncOn
.
stop :: Ord k => Rapid k -> k -> x -> IO () Source #
Delete the thread with the given name.
When an update occurs and the thread is currently running, it is cancelled.
Communication
:: (Ord k, Typeable a) | |
=> Rapid k | Rapid state handle. |
-> k | Name of the mutable variable. |
-> IO a | Action to create. |
-> IO a |
Get the value of the mutable variable with the given name. If it does not exist, it is created and initialised with the value returned by the given action.
Mutable variables should only be used with values that can be
garbage-collected, for example communication primitives like
MVar
and TVar
, but also pure run-time
information that is expensive to generate, for example the parsed
contents of a file.
Delete the mutable variable with the given name, if it exists.
:: (Ord k, Typeable a) | |
=> Rapid k | Rapid state handle. |
-> k | Name of the mutable variable. |
-> IO a | Value action. |
-> IO a |
Overwrite the mutable variable with the given name with the value returned by the given action. If the mutable variable does not exist, it is created.
This function may be used to change the value type of a mutable variable.