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.
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.
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
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 setModel =<< readModel (FSNotify.eventPath evt) loop loop where readModel fp = do s <- readFileLBS fp case toList <$> Csv.decode Csv.NoHeader s of Left err -> throw $ userError err 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.