From ca718be4b6d2a18e8a75c844e401a8020ea0e1fa Mon Sep 17 00:00:00 2001 From: Andreas Ekeroot Date: Mon, 25 Sep 2023 13:31:14 +0200 Subject: [PATCH] Flatten the main function This should make it a bit easier to read while hopefully making it easier to change in the future. Use pattern matching to see if the user wants to the see the usage instructions, this allows us to make use of one `case` expression instead of a `case` expression and an `if` expression. Do configuration reification pointfree style while using lenses. Co-authored-by: Jacob "Jassob" Jonsson --- app/Main.hs | 95 +++++++++++++++++++++++++-------------------------- src/Config.hs | 16 ++++++++- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 37b341e..0b2ccb7 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -2,15 +2,14 @@ module Main ( main - ) -where + ) where import Control.Concurrent ( MVar , newMVar , threadDelay , tryPutMVar ) -import qualified Control.Concurrent.Async as Async +import qualified Control.Concurrent.Async as Async import Control.Monad ( forever ) import Control.Monad.Log ( defaultBatchingOptions , renderWithTimestamp @@ -20,6 +19,7 @@ import Control.Monad.Log ( defaultBatchingOptio import Control.Monad.Reader ( runReaderT ) import Control.Monad.Trans ( liftIO ) import Data.FileEmbed ( embedDir ) +import Data.Foldable ( traverse_ ) import Data.IORef ( IORef , readIORef ) @@ -27,8 +27,7 @@ import Data.Time.Format ( defaultTimeLocale , formatTime , iso8601DateFormat ) -import Lens.Micro.Platform ( (<&>) - , set +import Lens.Micro.Platform ( set , view ) import Network.HTTP.Client.TLS ( newTlsManager ) @@ -56,63 +55,63 @@ import View ( render ) opts :: [OptDescr (Config -> Config)] opts = - [ Option [] ["help"] (NoArg (set cHelp True)) "Show usage info" - , Option [] ["port"] (ReqArg (set cPort . read) "PORT") "Port to run on" + [ Option [] ["help"] (NoArg (set cHelp True)) "Show usage info" + , Option [] ["port"] (ReqArg (set cPort . read) "PORT") "Port to run on" , Option [] ["interval"] (ReqArg (set cInterval . (1000000 *) . read) "INTERVAL (s)") "Update interval" ] +usage :: IO () +usage = putStrLn $ usageInfo "mat-chalmers [OPTION...]" opts + main :: IO () -main = - getArgs - <&> getOpt Permute opts - >>= \case - (_ , _ , _ : _) -> usage - (_ , _ : _, _ ) -> usage - (!confs, _ , _ ) -> do - let config = foldl (flip id) defaultConfig confs - if view cHelp config - then usage - else do - upd <- newMVar () -- putMVar when to update - mgr <- newTlsManager - (viewRef, refreshAction) <- runLoggingT - (runReaderT refresh (ClientContext config mgr)) - print +main = (reifyConfig . getOpt Permute opts <$> getArgs) >>= \case + (_ , _ , _ : _) -> usage + (_ , _ : _, _ ) -> usage + (Config { _cHelp = True }, _ , _ ) -> usage + (config , _ , _ ) -> do + upd <- newMVar () -- putMVar when to update + mgr <- newTlsManager + (viewRef, refreshAction) <- runLoggingT + (runReaderT refresh (ClientContext config mgr)) + print + + -- In the list there are three items running concurrently: + -- 1. Timer that sends a signal to the updater when it's time to update + -- 2. Webserver that serves the menus to the user + -- 3. Updater that fetches new data from the restaurants + Async.runConcurrently $ traverse_ + Async.Concurrently + [ timer upd config + , webserver config viewRef upd + , updater mgr upd refreshAction config + ] + where + timer upd cfg = + forever $ tryPutMVar upd () >> threadDelay (view cInterval cfg) - Async.concurrently_ - (Async.concurrently_ - -- timer - (forever $ tryPutMVar upd () >> threadDelay (view cInterval config)) - -- webserver - (serve config viewRef upd)) - -- updater - (forever - $ withFDHandler defaultBatchingOptions stdout 1.0 80 - $ \logCallback -> - runReaderT (refreshAction upd) (ClientContext config mgr) - `runLoggingT` ( logCallback - . renderWithTimestamp - (formatTime - defaultTimeLocale - (iso8601DateFormat - (Just "%H:%M:%S") - ) - ) - id - )) - where usage = putStrLn $ usageInfo "mat-chalmers [OPTION...]" opts + updater mgr upd refreshAction cfg = + forever + $ withFDHandler defaultBatchingOptions stdout 1.0 80 + $ \logCallback -> runLoggingT + (runReaderT (refreshAction upd) (ClientContext cfg mgr)) + ( logCallback + . renderWithTimestamp + (formatTime defaultTimeLocale + (iso8601DateFormat (Just "%H:%M:%S")) + ) + id + ) -serve +webserver :: Config -> IORef View -- ^ View model -> MVar () -- ^ Update signal -> IO () -serve conf viewRef upd = scotty (view cPort conf) $ do +webserver conf viewRef upd = scotty (view cPort conf) $ do middleware logStdout middleware (static $(embedDir "static")) get "/" ((html . render) =<< liftIO (readIORef viewRef)) get "/r" (liftIO (tryPutMVar upd ()) >> redirect "/") -- force update - diff --git a/src/Config.hs b/src/Config.hs index 3e548cd..4822482 100644 --- a/src/Config.hs +++ b/src/Config.hs @@ -4,6 +4,7 @@ module Config where +import Data.Foldable ( foldl' ) import Lens.Micro.Platform -- | Configuration record @@ -12,9 +13,22 @@ data Config = Config , _cNextDayHour :: !Int , _cInterval :: !Int , _cPort :: !Int - } deriving (Eq, Show) + } + deriving (Eq, Show) makeLenses ''Config defaultConfig :: Config defaultConfig = Config False 14 (1000000 * 60 * 30) 5007 + +-- | Create a Config we can touch +-- +-- Yes, this function implementation is overly cute. It uses a lens to update +-- the first field of the tuple by using defaultConfig as first argument to +-- every function in the list. The brain needs to turn inside out a couple of +-- times before it makes sense, so if you're in a hurry, look at the types. +-- +-- TODO: Feel free to bikeshed the function name. +reifyConfig + :: ([Config -> Config], [String], [String]) -> (Config, [String], [String]) +reifyConfig = (& _1 %~ foldl' (flip id) defaultConfig)