This pattern is design for qualified import this is why all types are called the same (Database.Handle, Socket.Handle, Database.Config, Secket.Config, ...)
What you need:
- A type called Handle
data Handle = Handle
{ hPool :: Pool Postgres.Connection
, hCache :: IORef (PSQueue Int Text User)
, hLogger :: Logger.Handle -- Another handle!
, …
}
-- Use it qualified: Database.createUser
createUser :: Handle -> Text -> IO User
createUser = …
getUserMail :: Handle -> User -> IO [Mail]
getUserMail = …
- All functions take the Handle as a first arguments:
-- KO
Database.createUser (userName <> "@" <> companyDomain) database
-- OK
Database.createUser database $ userName <> "@" <> companyDomain
- We need a configuration for our handle (usually pure code):
-- This is up to you
parseConfig :: x -> y -> IO Config
parseConfig = ....
data Config = Config
{ cPath :: FilePath
, …
}
-- Useful
instance Monoid Config
Always provide a function called new
:
new :: Config -- 1. Config
-> Logger.Handle -- 2. Dependencies
-> … -- (usually other handles)
-> IO Handle -- 3. Result
You don't usually want to wait until the GC destroys the reference and frees the resources.
It's a good idea to provide a close
function:
close :: Handle -> IO ()
close = …
It's usually a good idea to provide this. Even only providing (hiding new, close) is fine.
-- Implement it using bracket
withHandle
:: Config -- 1. Config
-> Logger.Handle -- 2. Dependencies
-> … -- (usually other handles)
-> (Handle -> IO a) -- 3. Function to apply
-> IO a -- 4. Result, handle is closed automatically
module MyApp.Database
( Config (..) -- Internals exported
, parseConfig -- Or some other way to load a config
, Handle -- Internals usually not exported
, new
, close
, withHandle
, createUser -- Actual functions on the handle
, …
) where
module MyApp.Database
( Handle (..) -- We now need to export this
) where
data Handle = Handle
{ createUser :: Text -> IO User
, …
}
-- What’s the type of createUser now?
createUser :: Handle -> Text -> IO User
-- It's exactly the same as before
-- | Create JSON-RPC session around conduits from transport layer.
-- When context exits session disappears.
runJsonRpcT
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> Ver -- ^ JSON-RPC version
-> Bool -- ^ Ignore incoming requests/notifs
-> Sink ByteString m () -- ^ Sink to send messages
-> Source m ByteString -- ^ Source to receive messages from
-> JsonRpcT m a -- ^ JSON-RPC action
-> m a -- ^ Output of action
-- v.s.
JsonRpc.Handle
Here is a real project using the Handle Pattern: https://github.com/jaspervdj/fugacious/blob/master/lib/Fugacious/Logger.hs
Source: https://jaspervdj.be/posts/2018-03-08-handle-pattern.html