Tightly coupled code is ill-advised in any codebase, and in Haskell there is also a drive to separate pure code from that causing or relying on side-effects*. So instead of building directly upon our foundation of
App**, an abstraction boundary was established above it to decouple the application code from
IO and from the exact storage location of the application settings.
(For a little more discussion on isolating side-effecting code via type-classes or other methods, check out this post on Alexis King's blog, specifically the Typeclasses can emulate effects section.)
As part of the abstraction boundary, the following type-classes were created for the
App type to implement to abstract things from the
AudioLoaderfor reading/writing to wave files
AudioPlayerfor audio playback control
Containerfor manipulating/reading shared application state
Operationfor running threads that report progress
Application settings were abstracted using the 'Has' pattern (Reddit discussion), so three more type-classes were created for the
GlobalContext to implement:
In this way the configuration concerns were separated into multiple cohesive interfaces, implementing Interface Segregation (SE discussion). Note that these three might be divided into more fine-grained interfaces in future.
* Decoupling and making code pure leads to easier reasoning, more flexibility, and easier testability.
type App = ReaderT GlobalContext IO