Hacker News new | ask | show | jobs
by thinkpad20 3551 days ago
If `function2` needs to log something, then it should have a type signature which reflects that.

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.

1 comments

> If `function2` needs to log something, then it should have a type signature which reflects that.

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?

> The fact that `function2` is logging stuff is an implementation detail that callers shouldn't care about.

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...
> > The fact that `function2` is logging stuff is an implementation detail that callers shouldn't care about.

> On the contrary, I think it definitely matters. If a function is going to log something, I want to know about it.

You are missing the forest for the trees.

First of all, why you'd care that a function you're calling is logging stuff is a bit beyond me but fine. Think of something else. Maybe the function is calling memcache, or storing stuff in the database, or sending a UDP packet to a message bus, or is querying the location service. Surely you can agree that there are things this function does that you don't care about if all you need is an Account given a user id, right?

These things you don't care about are called implementation details. Callers shouldn't know about them, therefore they shouldn't have to pass them in parameters.

That's what dependency injection (injection, not passing) does for you. It lets you call

    val account = getAccount(userId)
instead of

    val account = getAccount(userId, logger, memCache, db, messageBus)
The first example is using dependency injection and correctly hides the implementation details of `getAccount` while not being referentially transparent.

The second example is referentially transparent but exposes all kinds of private implementation details, making the callers' life very difficult, if not impossible (how are they supposed to come up with a messageBus when all they have is a user id?).

I agree on the benefits of the first example but is that really dependency injection? Or is it just an abstraction layer?

eg. what if getAccount was hard coded to initialize all the other dependencies it needed on the fly for each call?

If that's still considered DI then it's a much looser term than I understood it to be.