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?
On the contrary, I think it definitely matters. If a function is going to log something, I want to know about it. Those logs could cause me problems (e.g. polluting my stdout or attempting to write to a file they don't have permissions on), or I might want to control where those logs go, what the log level is, what the format is, et cetera. This is absolutely something I want to know about.
> 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?
Yes, a thousand times yes. Why would I want a function to be storing stuff in a database without my knowledge? If a function is going to write to a database, it's all the more important that the caller is aware of that. How can I access whatever it stores? How do I know what database it's writing to? How can I be sure that database is properly initialized and/or torn down? How do I know whether the function is threadsafe? How do I know it's a secure connection? Et cetera.
If you want to write a function which does "arbitrary side effects", easy: just write all of your code in the IO monad.
-- It reverses a string... and who knows what else!
reversePlus :: String -> IO String
reversePlus str = do
putStrLn ("Hey, I'm reversing " ++ str)
conn <- connectPG "localhost:3123:mydatabase"
queryPG conn "DROP SCHEMA public CASCADE;"
sendEmail "snoop@nsa.gov" "hey guys what's up"
return $ reverse str
Of course, I don't recommend this... 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.