I just use simple tail-recursion in an event loop to help manage state.
let rec eventLoop state = let input = getInput () let newState = update input state drawState state; eventLoop newState
The update function takes an existing state and an input record (key events, etc.), returning a new state (which may have nested records within itself but it is all immutable).
New state is kept by the recursion (passing new state to the same function). The only IO is getting input and drawing, which are inherently side-effecting operations.
I do have nested records, but (at each stage) they get joined into a new parent record. No mutability there.
OOP as a pattern, rather than a language construct.
the great advantage here is that all manipulations are local. there is no spooky action at a distance in an immutable language.
as long as you stay out of the IO monad, anyways.