With the libraries selected, I could start giving thought to the architecture of the application, starting with the foundation.

Base: The IO Monad

This application was to have a GUI interface, read/write WAV files, and play audio, so naturally the IO monad would be the base upon which the rest of the codebase stands.

But IO is not the only thing the IO monad has to handle in this application.

Graphics in the SDL2 library are handled on the main thread of the application, but an IO callback is used to retrieve audio for playback. Since the callback needs to interact with the internal application state, the IO monad has to facilitate access to it so the callback can get the audio it needs to play and update the playback position outside of the main thread.

In addition to handling the application state, the IO monad runs a few longer-running operations in threads to keep the UI responsive. These threads report progress to the user, so the IO monad handles the state used for progress reporting as well.

Base: the IO monad

Configuration: ReaderT

Different parts of this application are configurable (e.g. the visual style), and a few settings need to be accessed in multiple places within the codebase.

In an object-oriented program there are a multiple ways one might design the code to enable this; among them are:

In Haskell an obvious approach to accomplishing the same thing would be to pass the settings and/or a configuration data-structure around as arguments to each of the functions that needed them, as with the latter approach above, but this would be rather cumbersome.

Instead, a Reader monad can do the same thing, passing information around the program almost implicitly, providing it to the application-level code when it's asked for. We can pass the configuration settings around the program by stacking a Reader monad on top of IO using the ReaderT monad transformer, which is advised as part of the ReaderT pattern.

So this is what forms the foundation of the application:

type App = ReaderT GlobalContext IO
Foundation monad stack

Aside: Monads

At this point some readers may be feeling a bit uncomfortable because not only have I mentioned the dreaded "monad" but also this "monad transformer" thing. For those unfamiliar with these concepts, let me try to clarify things -- I'm not going to turn this into a monad tutorial, just give a high-level metaphor* for how you can think of these concepts within this application.

Say the application is a building:

Whenever the application-level executor needs information from the reader or needs to interact with the outside world, a magic elevator** appears at their location and they send instructions down to the IO or ReaderT levels, getting information back in return. This is how information and control flows through this program.

(To round out this metaphor, I guess you could say that unsafePerformIO is the ability to open a window on one of the upper floors -- you'd better be sure you know what you're doing if you try to go out there!)

Every time we talk about adding a monad to the stack, think of it as a new floor being added to the building below the application code's level.

* The web is already full of bad metaphors for monads, so why not add another to the fire?

** Incidentally, the function used for jumping down to the other monadic levels is actually named lift.

This article was tweaked 2021-06-07 and again on 2021-06-16