This is kind of a roundabout and softcore alternative answer to the excellent ones provided, but the problem I find with event-handling combined with side effects is that it makes it difficult to reason about exactly what happens and relatively when (in relation to something else of note).
Event-handling is about the most unpredictable kind of control flow I've encountered, because it's difficult to know exactly when some piece of code will be executed without understanding the lowest level details of the system (which may include the operating system or third party libraries). The ideal is that it's not supposed to matter when such code is invoked, but when it's combined with tricky side effects that need to occur at a precise order and time (at least relatively with something else), it can be very difficult to reason about the correctness of the system and the changes you want to make to it.
Homogeneous Side Effects In Simple Loops
Meanwhile consider this:
for each texture in textures_to_unload:
That's pretty easy to reason about, yes? So that's the goal as I see it when mutability is required. We want to get the control flow as dead-simple as possible with a very homogeneous side effect occurring so that we can easily see what's going on and when in relation to something else (not necessarily in an absolute context since this might be occurring in different threads, but at least in relation to some other states and side effects).
I've found this strategy very helpful in making side effects easier to reason about, and I utilize it by gathering a deferred list of things to uniformly affect out of the complex control flows (cascading events, graph data structure traversal, a combo of both, etc), like a concurrent list of textures to remove instead of removing them right away. Then in a place where it's easy to reason about the correctness of removing textures, I loop through the list and remove them.
Also while it carries the overhead of gathering these lists/sets of things to uniformly process in a deferred fashion, it often opens up more opportunities to parallelize some hefty work.
"More Homogeneous" Event Queues
An alternative is to gather a concurrent queue of side-effect-causing events to perform related to textures, e.g., which isn't quite as easy to reason about since it's not so homogeneous in terms of the side effects it causes, though it might still be easier to reason about than causing the side effects directly in the original events, since it's at least "more homogeneous".
The events in that texture-related queue might at least relate only to side effects associated with textures and nothing else, and the events triggered would only relate to textures and nothing else. So it's at least a little more predictable than doing things to textures directly as a result of a user input event triggered by the OS which triggers a widget event which causes the scene graph to be traversed which causes scene graph events to be called of which one of them causes a texture to be removed only to find that the display context is not valid at the time.
You might use this strategy if there are relative order dependencies among the operations being deferred.
Is there a design I could apply here to reduce the mutability in my
game? Ideally all mutations will be captured in a monad and applied
each iteration of the game loop in a single spot in the code.
I would suggest if there are mutable designs involved that the goal isn't so much to make everything executed in a single spot but to make things executed in a more homogeneous/uniform and predictable fashion if you need side effects to occur.
For example, with OGL and DX at least, you cannot perform texture operations without a valid display context. That's a relative kind of order dependency: one thing must be in a certain state before the other thing can occur. So deferring processing related to textures can allow you to do all texture-related operations in a spot in code where you can more easily ensure and reason that a valid display context is available.
As another example an indexed mesh might require that polygons are removed or rebuilt to exclude those vertices before the vertices it indexes are removed. Again we have an order dependency, and deferring the removal of vertices to a place where we can more easily ensure that the polygons are no longer indexing those vertices can make the code much easier to reason about.
But you don't necessarily have to make all side effects in the entire system happen in one place to achieve that, and it's not necessarily so helpful to seek that, since then you have a boatload of non-homogeneous side effects interleaved in one place, and that too may not be so easy to reason about. This is assuming you're still dealing with a lot of mutable designs instead of going towards like a hardcore pure functional route revolving around immutable data types and persistent data structures. If you don't go all the way there, an easier goal that often helps things a whole lot is just to seek more homogeneous side effects being applied uniformly with very simple control flows in places where you can easily reason about the order in which things relatively occur.