Use pipes
. I won't say it is idiomatic because the library is still relatively new, but I think it exactly solves your problem.
For example, let's say that you want to wrap an interface to some database:
import Control.Proxy
-- This is just some pseudo-code. I'm being lazy here
type QueryString = String
type Result = String
query :: QueryString -> IO Result
database :: (Proxy p) => QueryString -> Server p QueryString Result IO r
database = runIdentityK $ foreverK $ \queryString -> do
result <- lift $ query queryString
respond result
We can then model one interface to the database:
user :: (Proxy p) => () -> Client p QueryString Result IO r
user () = forever $ do
lift $ putStrLn "Enter a query"
queryString <- lift getLine
result <- request queryString
lift $ putStrLn $ "Result: " ++ result
You connect them like so:
runProxy $ database >-> user
This will then allow the user to interact with the database from the prompt.
We can then switch out the database with a mock database:
mockDatabase :: (Proxy p) => QueryString -> Server p QueryString Result IO r
mockDatabase = runIdentityK $ foreverK $ \query -> respond "42"
Now we can switch out the database for the mock one very easily:
runProxy $ mockDatabase >-> user
Or we can switch out the database client. For example, if we noticed a particular client session triggered some weird bug, we could reproduce it like so:
reproduce :: (Proxy p) => () -> Client p QueryString Result IO ()
reproduce () = do
request "SELECT * FROM WHATEVER"
request "CREATE TABLE BUGGED"
request "I DON'T REALLY KNOW SQL"
... then hook it up like so:
runProxy $ database >-> reproduce
pipes
lets you split out streaming or interactive behaviors into modular components so you can mix and match them however you please, which is the essence of dependency injection.
To learn more about pipes
, just read the tutorial at Control.Proxy.Tutorial.