Mi piacerebbe offrire una prospettiva leggermente diversa che potrebbe aiutarti con il tuo problema attuale.
Non pensare che i cambiamenti di stato siano negativi. Dopo tutto, se non ci fossero stati cambiamenti nel tuo programma, potresti tranquillamente sostituirlo con un no-op.
Il problema con il cambiamento di stato è che è notoriamente difficile ragionare sullo sviluppatore e sul sistema. Ciò è peggiorato se le modifiche dello stato sono ampiamente diffuse attraverso il programma e soprattutto se sono implicite o nascoste.
Ciò che la programmazione funzionale promuove con concetti come la purezza e l'immutabilità non è tanto la rimozione del cambiamento di stato. Si tratta di rendere il cambiamento di stato trasparente e incapsulato.
Quindi, per fare il tuo esempio, un approccio sarebbe quello di avere la logica che genera i comandi DB semplicemente accumulare i comandi ma non eseguirli. Una volta che li hai tutti, puoi avere un componente che esegue i comandi.
Inoltre, se la tua logica di generazione accetta l'attuale elenco di comandi e restituisce un nuovo elenco, con le modifiche apportate, hai purezza nella generazione. Il vantaggio è che è più facile ragionare e testare le tue funzioni logiche.
Potrebbe sembrare che tutto ciò che hai fatto sia spostare la complessità e, in qualche misura, è vero. Ma nota, il codice che gestisce la mutabilità è piuttosto specifico: non ha alcuna logica di generazione, esegue solo comandi. Tutto ciò di cui avevi bisogno per cambiare il comando del comando wrt, ecc. Verrebbe incapsulato lì.
Dove le monadi tendono ad essere utili è quando hai bisogno di mantenere l'ordine e le informazioni sulla dipendenza, cioè se riesci a capire cosa devi fare, fallo subito e non sono molto utili. Ma si noti, nello schema sopra, c'è un ritardo concettuale tra la generazione e l'esecuzione in modo che possano essere utili. Una volta che consideri i percorsi di errore, il loro rigore può essere inestimabile.
E, in pratica, è il ritardo tra le azioni che trovo sia un buon indicatore che una monade può essere utile. Anche al di fuori delle lingue funzionali. Ad esempio, la gestione degli errori e il codice asincrono spesso traggono vantaggio dalle monadi. interessante, queste sono le aree in cui le monadi hanno avuto crossover, ad es. i futures sono abbastanza comuni nelle lingue non funzionali.
Per inciso, questa è la mia opinione sul motivo per cui le monadi sono state particolarmente prevalenti in Haskell. Non è tanto che il sistema di tipi possa esprimerli bene, anche se questo aiuta. È che, a causa della pigrizia di Haskell, quasi tutto ha un ritardo tra l'esecuzione del codice e l'effetto che si verifica.