A typical way to organize Haskell programs is to create an application-specific monad that manages the effects that are needed for your application domain. This can be done by imposing the necessary functionality on what we call the "monad transformer stack". If IO is widespread in the application, then IO can be specifically set as the base of the stack (this is the only place that can fit, as this is the only monad that cannot be deconstructed by user level code), but the basic monad can often be left behind Abstract, which means you can instantiate using the non-IO monad for testing.
To be more specific, monad transformer stacks are often created using a set of standard transformers, known as Reader, Writer, and State. Each of them provides a different βeffectβ that implicitly penetrates through the code written in this monad. For your journal purposes, the Writer monad is often used (in its transformer form, WriterT); it is essentially the monoid you provide, which creates some output based on calls to its tell method. If you implement a tell based log function, then any function in your application monad can have a mappend ed log message to log out. Reader functions are often used to provide a set of fixed environmental parameters using the ask method; The condition is pretty obvious; it passes some convertible data types through your application and allows your application to convert it using the get and put methods. Other monad transformers are also provided by libraries; EitherT can provide exceptional features for your application, ListT can provide non-determinism through Monad List, etc.
With such a transformer stack, you usually want to limit it to the level of "application logic" of your program, so you do not need any functions in your application monad that do not need effects. Regular modular programming practice is applied; keep your abstractions loosely coupled and very cohesive and provide them with functionality using normal pure functions so that the application logic can work with them at a high level of abstraction. For example, if you have the concept of Person in your business logic, you will implement data and functions about the Person in Person module, which does not know anything about your application monad. Just perform your functions, which may fail to return any value with enough information to make a log entry; when your application logic manipulates Person values ββwith these functions, it can match patterns by result or work in the EitherT monad on the fly if you need to combine several possible failures. You can then use your Writer-based logging features at your application level.
The top level of every Haskell program is always in the IO monad. If you made your stack abstract in relation to its Monad base or just made it completely, then you will need a small top level to provide the IO functionality that your application needs. You can run the monad step application at a time if it is interactive, in which case you can unzip Writer to receive log entries and possibly information about other IO actions requested by the application logic. Results can be returned to the application environment through the Reader or State level. If your application is a batch processor, you can simply provide the necessary inputs through the results of I / O operations, run the application monad, and then upload the journal from Writer via IO.
The purpose of all this is to show that monads and monad transformers allow you a very clean way to separate the different parts of real-world applications so that you can make full use of pure simple functions to convert data in most places and leave your code very clean and verifiable. You can sequestrate I / O operations at a small level of "run-time" support, application-specific logic in a similar but larger layer, built around a (possibly clean) monad transformer stack, and manipulate business data in a set of modules on which Do not rely on any application logic functions that use them. This allows you to easily reuse these modules for applications in a similar domain later.
Getting a structured program structure in Haskell requires practice (as in any language), but I think you will find that after reading a few Haskell applications and writing a few of your own functions that it provides, you can create very well structured applications that are incredible easy to expand and reorganize. Good luck with your efforts!