I'm trying different approaches to do what is sometimes known as dependency injection. For this I've elaborated a simple example of a weather app, where we want to fetch the weather data (from a web-service or from a hardware device), store the weather data (could be a database or simply a file), and report it (either print it to screen, or speak the weather). The idea is to write a program that uses some fetch
, store
, and report
functions, whose implementations can vary.
I've managed to separate concerns and abstract away from the implementations of retrieval, storage, and reporting using functions and free-monads, however the solution I reached with monad stacks looks bad:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module WeatherReporterMTL where
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
type WeatherData = String
class Monad m => WeatherService m where
fetch :: m WeatherData
class Monad m => Storage m where
store :: WeatherData -> m ()
class Monad m => Reporter m where
report :: WeatherData -> m ()
-- | A dummy implementation of the @WeatherService@
newtype DummyService m a = DummyService { runDummyService :: m a }
deriving (Functor, Applicative, Monad, MonadIO)
instance MonadIO m => WeatherService (DummyService m) where
fetch = return "won't get any warmer in December."
-- | A dummy implementation of the @Storage@
newtype DummyStorage m a = DummyStorage { runDummyStorage :: m a }
deriving (Functor, Applicative, Monad, MonadIO, WeatherService)
-- It seems wrong that the storage has to be an instance the weather service
-- (@WeatherService@) ...
instance MonadIO m => Storage (DummyStorage m) where
store d = liftIO $ putStrLn $ "No room left for this report: " ++ d
-- | A dummy implementation of the @Reporter@
newtype DummyReporter m a = DummyReporter { runDummyReporter :: m a }
deriving (Functor, Applicative, Monad, MonadIO, WeatherService, Storage)
-- Ok, now this seems even worse: we're putting information about
-- how we're gonna stack our monads :/
instance MonadIO m => Reporter (DummyReporter m) where
report d = liftIO $ putStrLn $ "Here at the MTL side " ++ d
reportWeather :: (WeatherService m, Storage m, Reporter m) => m ()
reportWeather = do
w <- fetch
store w
report w
dummyWeatherReport :: IO ()
dummyWeatherReport = runDummyService $ runDummyStorage $ runDummyReporter reportWeather
In the code above, both DummyStorage
and DummyReporter
have to have trivial instances for WeatherService
, which seems plainly wrong. Moreover, these instances depend on the order monads are stacked in the end. Is there a way to avoid leaking information between the different stacks?
GeneralizedNewtypeDeriving
magic?). – IrenicsMonadTrans
. eg,instance MonadTrans DummyReporter where lift = DummyReporter
. I'm afraid I don't understand your question about cross-cutting concerns – MerovingianreportWeather
above. Maybe orthogonal-concerns is a better description. – IrenicsWeatherReporterFreeSeparated
). It is a well known limitation of monad transformers that you needn*m
instances (wheren
=the number of transformers andm
=number of classes) - e.g.ReaderT
has 'trivial' instances forMonadWriter
,MonadState
, etc. However, it isn't true that order matters (you can have instances for all 3 classes for all 3 types). Unless there is a compelling reason to use monad transformers, I would recommend you use the solution you've found already. – Panefetch >> store >> report
is business logic. And as I exemplified in the question,fetch
might use an online service or some hardware,store
might use a database or a file,report
might print to screen or speak the weather. So I was wondering how these implementation specific details could be abstracted using MTL (instead of functions or free monads for instance). – Irenics