- All your functions now need to return a Reader[C,A] instead of just A
- You need to pass all the parameters explicitly in each method signature as opposed to passing just the ones that don't need to be injected.
-- This function will always return the same thing given the same input
function1 :: Int -> String
-- This function depends on reading some configuration which
-- needs to be provided upstream
function2 :: Int -> Reader Configuration String
You don't need to pass parameters explicitly in each signature; indeed this is exactly what the reader monad obviates: the details of what is being read are not expressed inside the function (until the point that they they are actually used). This is hardly an onerous burden, in my opinion. And if typing `Reader X Y` is too annoying, you can just make a type alias.Logging is a side-effect. Logging requires configuration to be passed in; it means having access to some file descriptor or other object to interact with, it could potentially fail to connect, or cause a computation to hang, or cause a service to trigger, or make a disk run out of space, etc. If a function wants to log something it's not a simple reader anymore but something more complex. The fact that in Haskell this is reflected in the type signature of the function is again a good thing. It's not "polluting" the method signature; it's putting more information in the method signature. Not letting you hide side effects in a computation that appears to have no externalities is a strength of Haskell, not a weakness.
Yes if you value referential transparency.
No if you value encapsulation.
The fact that `function2` is logging stuff is an implementation detail that callers shouldn't care about. They should certainly not be forced to pass that function a logger.
What if that function decides that on top of logging, it wants to store stuff in a database. Should all callers suddenly find some kind of database to pass to that function too?
class (MonadIO m) => HasLogging m where
log :: String -> m ()
data AppConfig = AppConfig { stuff :: Int }
newtype MyApp a = MyApp { runApp :: ReaderT AppConfig IO a}
deriving (Functor, Applicative, Monad, MonadIO, MonadReader AppConfig)
instance HasLogging MyApp where
log s = liftIO (putStrLn s)
function2 :: Int -> MyApp String
function2 x = do
log "hey guys I'm logging"
return (show x)
-- or without specifying the base monad, yay abstraction
function2' x = do
log "heyooo logging here"
return (show x)
-- Haskell will infer this type:
-- function2' :: (HasLogging m, Show a) => a -> m StringWe are in strong disagreement about what constitutes an "implementation detail".
But also, you can just use a monad transformer stack and add whatever side-effectful operations you want into it, use it as needed. Boom, dependency injection. And more control over what your functions actually do is there when you need it.
This (ab)uses Haskell's type class mechanism to essentially implement dependency injection directly. The implementation looks a bit dirty, but this is a feature that more modern approaches to generic programming can handle natively (e.g., http://homepages.inf.ed.ac.uk/wadler/papers/implicits/implic... ).
In particular, there is nothing shady about the semantics of implicitly passing configuration values/dependencies. Your functions are still referentially transparent if you treat the implicit dependencies as additional parameters (which is what they are, no matter how you implement it).