In Add a Model, we modified our mood tracker to display the moods from a CSV file. Here, we improve it so that any user modifications to the data/moods.csv
file will hot reload the Live Server view of our app in the same manner as Emanote does.
Dynamic
To do this, we must understand what a Dynamic (which siteInput
returns) is.
siteInput
is defined to return a Dynamic m (RouteModel r)
. In our case, r ~ Route
and RouteModel Route ~ Model
, thus our siteInput
returns a Dynamic m Model
in the IO monad. A Dynamic is simply defined as:
newtype Dynamic m a
= Dynamic
( -- Initial value
a
, -- Set a new value
(a -> m ()) -> m ()
)
It is a pair of values: the initial value and a function that knows how to update that value over time using the user-provided update function (a -> m ()
). Dynamic’s are an Applicative
, so they compose using liftA*
family of functions.
In our siteInput
, so far, we return pure myModel
—it has an initial value but does not update over time. In order to return an actually updating Dynamic
of that model, we would change it to: Dynamic (myModel, updater)
and now the task becomes to define the “updater” function itself. Spelled out:
siteInput _ _ = do
let myModel = ...
pure $ Dynamic $ (myModel, ) $ \setModel -> do
let loop = do
let theNewModel = <some func that returns the next update>
setModel theNewModel
loop
loop
FSNotify
In the case of our mood tracker, we will use the fsnotify package (see unionmount
for another option) to fulfill that <some func that returns the next update>
part. So, without further ado, here’s the full implementation of the new siteInput
that produces a fully-fledged Dynamic m Model
:
siteInput _ _ = do
model0 <- readModel "data/moods.csv"
-- Create a `Dynamic` with initial value (model0) and an updater function
pure $ Dynamic $ (model0,) $ \setModel -> do
ch <- liftIO $ watchDirForked "data"
let loop = do
logInfoNS "fsnotify" "Waiting for fs event ..."
evt <- liftIO $ readChan ch
logInfoNS "fsnotify" $ "Got fs event: " <> show evt
handle (\(e :: IOException) -> logErrorNS "fsnotify" (show e)) $
setModel =<< readModel (FSNotify.eventPath evt)
loop
loop
where
readModel fp = do
s <- readFileLBS fp
case toList <$> Csv.decode Csv.NoHeader s of
Left err -> logErrorNS "csv" (toText err) >> pure (Model mempty)
Right moods -> pure $ Model $ Map.fromList moods
-- Observe changes to a directory path, and return the `Chan` of its events.
watchDirForked :: FilePath -> IO (Chan FSNotify.Event)
watchDirForked path = do
ch <- newChan
void . forkIO $
FSNotify.withManager $ \mgr -> do
_stopListening <- FSNotify.watchDirChan mgr path (const True) ch
threadDelay maxBound
pure ch
Now if you run the app and modify the data/mood.csv
file (e.g., change “Neutral” to “Bad”), your app’s web view will update in real-time. Your Ema app updates instantly on code or data change.
This concludes the tutorial series, and hopefully, you have gained an introductory understanding of what is entailed behind the “just about any app that creates a browser view of arbitrarily changing data” claim on the index page. You can view the source code for the mood tracker tutorial at https://github.com/srid/MoodTracker-Tutorial.
You may visit Guide or Topics to further your understanding.