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...
A value of type (IO a) is almost completely inert. In fact, the only IO action which can really be said to run in a compiled Haskell program is main . Armed with this knowledge, we can write a "hello, world" program:
main :: IO () main = do c <- getChar. putChar c. The use of the name main is important: main is defined to be the entry point of a Haskell program (similar to the main function in C), and must have an IO type, usually IO ().
Haskell separates pure functions from computations where side effects must be considered by encoding those side effects as values of a particular type. Specifically, a value of type (IO a) is an action, which if executed would produce a value of type a. Some examples:
In fact, the only IO action which can really be said to run in a compiled Haskell program is main . Armed with this knowledge, we can write a "hello, world" program: main :: IO () main = putStrLn "Hello, World!"
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 mappend
ed 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!
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With