There are lots of designs to look through!
It’s probably worth checking ST which has an interesting approach for keeping mutations “local” to a particular context. (It requires a feature that Elm does not have, and I don’t think that feature is in the “keep it simple” spirit of Elm.)
I recall hearing about transients in Clojure and wondering if it’d be possible to do something like that without the forall s trick. For example, maybe it’d be possible to have an API like:
type Transaction v a
set : Int -> v -> Transaction v ()
get : Int -> Transaction v v
andThen : (a -> Transaction v b) -> Transaction v a -> Transaction v b
...
run : Transaction v () -> Array v -> Maybe (Array v)
Basically, restrict the API such that a “mutable array” cannot escape the Transaction. I think one could explore this as a pure Elm API to figure out how many scenarios could be covered by something like this. How does append work as a transaction? Map? The actual optimization of transactions (e.g. combining many set calls into a single allocation) could be done after the API itself is shown to be workable.
This angle is also interesting in that I think this is something that comes up for people using Elm more often than other cases where mutation is helpful. (E.g. I need mutation for UnionFind in type inference, but that’s a pretty rare scenario!)
If the goal is to explore mutable references with more of a Task API, it’s good to check out IORef and MVar in Haskell. (I have grown to be a fan of MVar over time. The compiler uses them to manage concurrent compilation without extra queueing. It’s pretty cool!)
Anyway, just some ideas of what might be fun to look into!