Skip to content

Instantly share code, notes, and snippets.

@andrevdm
Last active March 26, 2018 19:18
Show Gist options
  • Save andrevdm/d2a741d5c67f4b421811134ed7df17a2 to your computer and use it in GitHub Desktop.
Save andrevdm/d2a741d5c67f4b421811134ed7df17a2 to your computer and use it in GitHub Desktop.
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Combined where
import Protolude
import qualified Data.ByteString as BS
import Control.Monad.Trans (lift)
import Control.Monad.Reader (ask, runReaderT, ReaderT, MonadReader, MonadTrans)
import Control.Concurrent.STM (TVar, modifyTVar', newTVar, atomically, readTVar, writeTVar)
--------------------------------------------------------------------------------------------------------
-- Layer 1 (orchestration)
--------------------------------------------------------------------------------------------------------
data AppState = AppState { apText :: !(TVar ByteString)
, apScore :: !(TVar Int)
, apDb :: !(TVar FilePath)
}
newtype AppT m a = AppT { unAppT :: ReaderT AppState m a
} deriving (Functor, Applicative, Monad, MonadReader AppState, MonadTrans)
askApp :: (AppState -> TVar a) -> AppT IO a
askApp getter = do
app <- ask
lift . atomically . readTVar $ getter app
modifyApp :: (AppState -> TVar a) -> (a -> a) -> AppT IO ()
modifyApp getter modify' = do
app <- ask
lift . atomically $ modifyTVar' (getter app) modify'
--------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------
-- Layer 2 (bridge / external services)
-- Avoid IO by using mtl
--------------------------------------------------------------------------------------------------------
class (Monad m) => MonadModel m where
loadModel :: m ByteString
storeModel :: ByteString -> m ()
type InMemDb = ReaderT (TVar ByteString)
instance MonadModel (InMemDb IO) where
loadModel = liftIO . atomically . readTVar =<< ask
storeModel x = liftIO . atomically . flip writeTVar x =<< ask
type OnDiskDb = ReaderT FilePath
instance (MonadIO m) => MonadModel (OnDiskDb m) where
loadModel = liftIO . BS.readFile =<< ask
storeModel x = liftIO . flip BS.writeFile x =<< ask
--------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------
-- Layer 3 (Business logic, pure code only)
--------------------------------------------------------------------------------------------------------
manipulatedData :: ByteString -> (ByteString, Int)
manipulatedData s =
let s' = s <> ".added" in
(s', BS.length s')
--------------------------------------------------------------------------------------------------------
-- ========
-- L1
-- ========
run :: IO ()
run = do
let dbPath = "db1.txt"
BS.writeFile dbPath "orig-file"
app <- atomically $ do
txt <- newTVar "orig"
score <- newTVar 0
db <- newTVar dbPath
pure AppState { apText = txt
, apScore = score
, apDb = db
}
let pipeline = startup >> process >> finalise
runReaderT (unAppT pipeline) app
startup :: AppT IO ()
startup = do
db <- askApp apDb
v1 <- lift $ runReaderT load db
modifyApp apText (const v1)
process :: AppT IO ()
process = do
txt <- askApp apText
-- E.g. of L1 calling L3 manipulateData, and L2 askApp & modifyApp
let (s1, v1) = manipulatedData txt
modifyApp apText (const s1)
modifyApp apScore (const v1)
finalise :: AppT IO ()
finalise = do
txt <- askApp apText
db <- askApp apDb
lift $ runReaderT (save txt) db
lift . print $ txt
-- ========
-- L2
-- ========
load :: (MonadModel m) => m ByteString
load = loadModel
save :: (MonadModel m) => ByteString -> m ()
save = storeModel
-- ========
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Separate where
import Protolude
import qualified Data.ByteString as BS
import Control.Monad.Trans (lift)
import Control.Monad.Reader (ask, runReaderT, ReaderT, MonadReader, MonadTrans)
import Control.Concurrent.STM (TVar, modifyTVar', newTVar, atomically, readTVar, writeTVar)
--------------------------------------------------------------------------------------------------------
-- Layer 1 (orchestration)
--------------------------------------------------------------------------------------------------------
data AppState = AppState { apText :: !(TVar ByteString)
, apScore :: !(TVar Int)
, apDb :: !(TVar FilePath)
}
newtype AppT m a = AppT { unAppT :: ReaderT AppState m a
} deriving (Functor, Applicative, Monad, MonadReader AppState, MonadTrans)
askApp :: (AppState -> TVar a) -> AppT IO a
askApp getter = do
app <- ask
lift . atomically . readTVar $ getter app
modifyApp :: (AppState -> TVar a) -> (a -> a) -> AppT IO ()
modifyApp getter modify' = do
app <- ask
lift . atomically $ modifyTVar' (getter app) modify'
--
demoAdd5 :: Int -> AppT IO Int
demoAdd5 x =
pure $ x + 5
demoStoreVal :: Int -> AppT IO ()
demoStoreVal v =
modifyApp apScore (const v)
dumpAppT :: AppT IO ()
dumpAppT = do
modifyApp apText (<> ".")
lift . print =<< askApp apText
lift . print =<< askApp apScore
lift . print =<< askApp apDb
--------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------
-- Layer 2 (bridge / external services)
-- Avoid IO by using mtl
--------------------------------------------------------------------------------------------------------
class (Monad m) => MonadModel m where
loadModel :: m ByteString
storeModel :: ByteString -> m ()
type InMemDb = ReaderT (TVar ByteString)
instance MonadModel (InMemDb IO) where
loadModel = liftIO . atomically . readTVar =<< ask
storeModel x = liftIO . atomically . flip writeTVar x =<< ask
type OnDiskDb = ReaderT FilePath
instance (MonadIO m) => MonadModel (OnDiskDb m) where
loadModel = liftIO . BS.readFile =<< ask
storeModel x = liftIO . flip BS.writeFile x =<< ask
--
demoDb :: MonadModel m => m ByteString
demoDb = do
v1 <- loadModel
let v2 = v1 <> ".loaded, "
storeModel v2
pure v2
demoInMemDb :: IO ()
demoInMemDb = do
m <- atomically $ newTVar ("orig-mem" :: ByteString)
r <- runReaderT demoDb m
print r
demoOnDiskDb :: IO ()
demoOnDiskDb = do
let dbPath = "db.txt"
BS.writeFile dbPath "orig-file"
r <- runReaderT demoDb dbPath
print r
--------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------
-- Layer 3 (Business logic, pure code only)
--------------------------------------------------------------------------------------------------------
manipulateData :: Text -> Text
manipulateData t =
t <> ".manipulated"
--------------------------------------------------------------------------------------------------------
run :: IO ()
run = do
-- Demo layer 1 only
app <- atomically $ do
txt <- newTVar "orig"
score <- newTVar 0
db <- newTVar "db.txt"
pure AppState { apText = txt
, apScore = score
, apDb = db
}
let fs = demoAdd5 10 >>= demoAdd5 >>= demoStoreVal >> dumpAppT
runReaderT (unAppT fs) app
-- Demo layer 2 only
demoInMemDb
demoOnDiskDb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment