6

Disclaimer: My ignorance about Haskell is almost perfect. Sorry if this is really basic, but I couldn't find an answer, or even a question like that. Also my English is not that good.

As far as I understand, if I have a function in a system that somehow interacts with filesystem this function must use the IO monad, and will have a type like IO ()

In my (only business oriented) experience, systems typically interact with filesystem for reading/writing files with business data, AND for logging.

And in business application, logging is everywhere. So if I write a system in Haskell (which I wont for a long while), pretty much every function will use the IO monad.

Is that the common practice or somehow logging do not requires IO ()? Or maybe Haskell business application do not log that much?

Also, how about other types of I/O? if I need to access a database or a web service from a function, this function also uses the IO monad or Haskell also has WS and DB monads? I'm almost sure there is only one IO monad... being able to know the kind of IO just looking at the type looks amazing from my point of view, but I'm sure my point of view is not an objective measure of usefulness...

DNA
  • 42,007
  • 12
  • 107
  • 146
Pablo Grisafi
  • 5,039
  • 1
  • 19
  • 29
  • You might be interested in an idea [I had about this topic a while back](http://stackoverflow.com/a/20319642/839246). This isn't done much in practice, but it'd be easy to implement. I even have a start at it on [Hackage](http://hackage.haskell.org/package/base-io-access), but I will warn that it isn't a mature library yet and subject to backwards incompatible changes like different naming schemes, although I have not had the time to work on it for the last 6 or 7 months. – bheklilr Jul 11 '14 at 15:03
  • A common pattern you'll see is most of these actions, such as database interaction, logging, and resource management have dedicated monads and monad transformers that can be used to build more complex monads relatively simply. This lets you write lots of monadic code that only knows about what it needs to. Having custom monads for performing these tasks also lets the programmer abstract away certain patterns and operations, making the code more intuitive and safer. – bheklilr Jul 11 '14 at 15:14
  • 4
    Typically, you would log side-effects (like "order created" or "mail sent"). In Haskell it is best to separate side-effects from computation. In these examples you would compute the order and mail data without the need for logging. Only when actually inserting the order or sending the mail you log this action. Those pieces of code need IO anyway. The computations do not. If you just slap IO on everything you still have pure functions but the code quality hasn't improved. – usr Jul 11 '14 at 15:16
  • Regarding "If you just slap IO on everything you still have pure functions but the code quality hasn't improved"... I know quality hasn't improved, but are Haskell programmers so used to never have issues? I mean, I log stuff because I know there can always be a corner case I wasn't expecting, a problem with my own code, an unexpected behavior in some library, a case that business people told me 'this can never happen' but eventually happens. I need to know, after a production failure, what made that failure possible, and the more info I got the better. Are errors so strange in Haskell? – Pablo Grisafi Jul 12 '14 at 02:41
  • @PabloGrisafi If you've logged the inputs you received (IO) and the actions you took (IO), and the rest is pure computation, then that's enough information. Pure computations (no matter how much code is involved) always produce the same output for a given input. So once you're aware that there was a failure in production and have tracked it down to a Haskell system taking the wrong action in response to correct input, you have a test case you can reproduce in dev with full debugging facilities to see how it computed the action to take incorrectly. – Ben Jul 12 '14 at 22:51
  • @Ben: This is true regarding the language, but Haskell compiler forces you to make pure computations really pure, right? My problem happens because computations and data gathering are too interleaved. To calculate a total, add every line...but for every line get an exchange rate via WS, then check if discount applies, that forces me to check if that total is in a given range => DB, and then check the customer credit status => WS, and then, and then... Still, I'm becoming more and more sure that Haskell (or maybe Ocaml? or even F#?) are better ways to handle this mess than my usual Java/C# – Pablo Grisafi Jul 14 '14 at 16:32

2 Answers2

5

A typical way to organize Haskell programs is to build an application-specific monad that manages the effects that are required for your application domain. This can be done by layering the needed functionality in what we call a "monad transformer stack". If IO is pervasive in the application, then IO may be concretely specified as the base of the stack (that is the only place it can fit, as it is the only monad that can't be deconstructed by user-level code), but the base monad can often be left abstract, meaning that you can instantiate it with a non-IO monad for testing.

To be more specific, monad transformer stacks are often built with a set of standard transformers known as Reader, Writer, and State. Each provides a different "effect" that is implicitly threaded through code written in that monad. For your logging purposes, the Writer monad (in its transformer form, WriterT) is often used; it's essentially a Monoid that you provide that builds up some output based on calls to its tell method. If you implement a log function based on tell, then any function in your application monad can have a log message mappended to the log output. The Reader functionality is often used for providing a set of fixed environmental parameters via the ask method; State is rather obvious; it threads some transformable data type through your application and allows your application to transform it via get and put methods. Other monad transformers are provided by libraries as well; EitherT can provide an exception-like functionality for your application, ListT can provide non-determinism via the List monad, etc.

With a transformer stack like that, you generally want to confine it to an "application logic" layer of your program, so that you don't require any functions to be in your application monad that don't need the effects. Regular modular programming practice applies; keep your abstractions loosely coupled and highly cohesive, and provide functionality in them via normal pure functions so that the application logic can operate on them at a high-level of abstraction. For example, if you have a notion of a Person in your business logic, you would implement data and functions about Person in a Person module that doesn't know anything about your application monad. Just make its functions that might fail return an Either value with enough information to make a log entry; when your application logic manipulates Person values with these functions, it can pattern-match on the result or work in an on-the-fly EitherT monad if you need to combine several possibly-failing functions. Then you can use your Writer-based logging functionality there in your application layer.

The top level of every Haskell program is always in the IO monad. If you have made your stack abstract with respect to its base Monad, or just made it pure altogether, then you'll need a small top-level to provide the IO functionality your application needs. You might run your application monad a step at a time if it's interactive, in which case you can unpack the Writer to get log entries and perhaps information about other IO actions requested by the application logic. Results can be fed back into the application environment via a Reader or State layer. If your application is just a batch processor, you might just provide the necessary inputs via the results of IO actions, run the application monad, then dump the log from the Writer via IO.

The point of all this is to illustrate that monads and monad transformers allow you a very clean way to separate various parts of real-world applications so that you can take full advantage of pure, simple functions for data transformation in most places and leave your code very clean and testable. You can sequester IO operations in a small "runtime support" layer, application-specific logic in a similar but larger layer built around a (possibly pure) monad transformer stack, and business data manipulation in a set of modules that don't rely on any features of the application logic that uses them. This lets you easily re-use those modules for applications in a similar domain later.

Getting the hang of program structuring in Haskell requires practice (as it does in any language) but I think you'll find that after reading through a few Haskell applications and writing a few of your own, the features it provides allow you to build very nicely-structured applications that are incredibly easy to extend and refactor. Good luck in your efforts!

Levi Pearson
  • 4,884
  • 1
  • 16
  • 15
  • Structuring Haskell programs is sometimes harder because you are *forced* to adhere to the principles that you should be using anyways. +1 – usr Jul 11 '14 at 21:42
  • You are forced to adhere to *some* of them anyway; there is still a wide variety of ways to make a mess of things, especially if you're not clear about the best way to divide things up yet. Fortunately, I've found that refactoring is almost unreasonably effective in my Haskell programming. – Levi Pearson Jul 12 '14 at 02:17
  • I'm sorry I didn't choose your answer, that I'm not able to discuss, because my Haskell level, as I said, is almost zero. Boyd's answer was closer to what I expected. (If you prefer a concrete example, you are not ready to Haskell yet, a teacher told me once). – Pablo Grisafi Jul 12 '14 at 02:21
  • 1
    That's fine! I hope you will revisit it as you get some more experience, though, because I think it will be helpful to you when you're ready for it. I may also update it with some examples; it is definitely suffering for lack of those now. – Levi Pearson Jul 12 '14 at 02:26
  • The question is: How does anybody get experience in Haskell? I can't spend time in something that is not usable in short/mid term, and Haskell is a long term investment. I'm going to ask in reddit/r/haskell, this is no place for such a question – Pablo Grisafi Jul 14 '14 at 16:40
  • 1
    I don't think it took particularly much longer for me to be able to write useful Haskell programs than it did for me to learn to write useful C programs. Which is to say they both took a while, not that Haskell was trivial to pick up. – Levi Pearson Jul 14 '14 at 16:55
3

Yes.

One way to do this is via a custom, restricted IO Monad. You create a newtype wrapper for (transformed) IO like so:

newtype MyIO a = My { _runMy :: IO a }

runMy :: MyIO a -> IO a
runMy = _runMy

However, you do NOT expose/export the My data constructor. Instead, you expose "wrapped" versions of the operations you want. You also do not expose a MonadIO instance (e.g.); that allows unrestricted lifting. You may or may not expose other instances to match IO. Basically, external users have to treat MyIO as just as opaque as built in IO, with only limited (i.e. restricted) conversion to MyIO. You DO expose a Monad instance, perhaps the one generated by GeneralizedNewtypeDeriving.

You DO expose the runMy function, which will allow embedding arbitrary MyIO actions inside general IO actions, such as at the "top-level" in main. You DO NOT directly expose the _runMy field, which (together with return) would actually provide a "backdoor" lift of:

backdoor :: IO a -> MyIO a
backdoor io = (return () :: MyIO ()) { _runMy = io }
-- polymorphic record update syntax for the win!

That said, most of my pure, total functions don't need to do logging, so I just log where I already have access to IO.

  • 2
    This is a nice approach for a "lightweight sandbox" kind of division of IO's functionality, but although it doesn't leave you with explicit `IO a` types in your signatures, you are still directly relying on the effects built in to IO and you won't be able to have a 'pure' version of your monad. – Levi Pearson Jul 12 '14 at 02:24
  • 1
    Yes, if you want to support alternative "evaluation" strategies other than "escape to IO" and then bind to main, you are better off building a functor for use in a free/operation monad. – Boyd Stephen Smith Jr. Jul 12 '14 at 16:34
  • I've used this design pattern for a few different purposes but so far not for logging (I've always used the `let (a,w) = runWriter ...; forkIO (hPutStrLn log w)` pattern) – Jeremy List Jun 08 '15 at 10:03