How do I do logging in Haskell?
Disclaimer: I'm the author of the Logger Haskell framework.
Although McCann's answer is greatly detailed, it does not tell, that Haskell was lacking a general purpose logging framework at the time the question was asked. The HSLogger is a standard now, but it provides very basic logging functionality while being slow and not extensible. To be clear, here are some defects of HSLogger:
- It is slow. By being slow I mean, that every time you log a message it parses (in a very simple way) a string describing the origin of the log and uses some existential datatypes under the hood, which have to introduce some performance overhead at runtime.
- It does not allow logging in other monads than IO, so you have to use
WriterT
or other solutions not to mess your code. - It is not extensible - you cannot create your own priority levels, define custom behaviours (like inter-thread logging) or compile time logs filtering.
- It does not provide some information, like line numbers or file names where the logs were placed. And of course it is very hard to extend it to support such information.
That being said I would love to introduce the Logger Haskell framework. It allows for efficient & extensible logging, including:
- logging in sequential pure code (performing as well as using
WriterT
monad) - advanced message filtering (including compile-time filtering)
- inter-thread logging ability
- provides
TemplateHaskell
interface allowing logging additional details, like file numbers or module names - is very easily extensible - all the features are created as extensions to a simple
BaseLogger
, which cannot do anything sensible. To be clear - the filtering functionality is created in less than 20 lines as a logger-transformer and you can define your own transformers. How to do it is described in the documentation. - Provides colored output on all platforms by default.
But the library is pretty new, so it can lack some needed functionality. The good information is, that you can create this functionality easily by yourself or help us improve it by reporting issues on GitHub.
The logger is developed internally by the company I'm working at (luna-lang.org) and is used inside a compiler we are creating.
Shameless plug: I'm the author of the co-log
logging library. You can find the code of the library and tutorials on GitHub:
- kowainik/co-log
The details of the library usage and implementation are described in the following blog post:
- co-log: Composable Contravariant Combinatorial Comonadic Configurable Convenient Logging
Don't be afraid of a scary name, the library is actually much simpler than it sounds :) The main idea behind co-log
is to treat logging actions as simple Haskell functions. Since functions are first-class citizens in Haskell and it is extremely easy to work with them.
First, a quick disclaimer: "logging" doesn't usually make sense in general Haskell code, because it assumes some sort of sequential execution that may or may not be meaningful. Make sure you distinguish between logging how the program executes and logging what values are computed. In strict imperative languages these are mostly the same, but in Haskell they aren't.
That said, it sounds like you want to log based on values being computed, in the context of an already sequential and stateful computation, which pretty much works the same as logging in most other languages does. However, you do need the monad to support some means of doing so. It looks like the parser you're using is from the HCodecs package, which seems to be relatively limited, doesn't allow IO
, and isn't defined as a monad transformer.
Honestly my advice would be to consider using a different parsing library. Parsec tends to be kind of the default choice, and I think attoparsec is popular for specific purposes (which might include what you're doing). Either would let you add logging much more easily: Parsec is a monad transformer, so you can put it on top of IO
and then use liftIO
as needed, whereas attoparsec is designed around incremental processing, so you can chunk your input and log aspects of the processing (though logging inside the actual parser may be more awkward). There are other choices as well but I don't know enough of the details to make a recommendation. Most parser combinator-based libraries tend to have fairly similar designs, so I'd expect porting your code would be straightforward.
A final option, if you really want to stick to what you've got, would be to look at the implementation of the parsing library you're using now and roll your own IO
-oriented version of it. But that's probably not ideal.
Also, as an addendum, if you what you're really after isn't actually logging but just tracing the execution of your program as part of development, you might find the debugger built into GHCi to be more helpful, or good old-fashioned printf debugging via the Debug.Trace module.
Edit: Okay, sounds like you have plausible reasons to consider rolling your own variation. What you roughly want here is a ParserT
monad transformer. Here's the current definition of Parser
:
newtype Parser a = Parser { unParser :: S -> Either String (a, S) }
The type S
is the parser state. Note that this is roughly a hard-coded version of StateT S (Either String) a
:
newtype StateT s m a = StateT { runStateT :: s -> m (a,s) }
...where Either String
is being treated as an error monad. The ErrorT
monad transformer does the same thing:
newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
So where the current type is equivalent to StateT S (ErrorT String Identity)
, what you want would be StateT S (ErrorT String IO)
.
It looks like most of the functions in the module aren't messing with the internals of the Parser
monad, so you should be able to simply replace the type definitions, supply the appropriate type class instances, write your own runParser
function, and be good to go.