"Cheating mutability" is trivial in Haskell. You import Data.IORef or Data.STRef and do your thing. (unsafePerformIO is just runST for "real world" actions.)
You'll find, though, that it's very easy to make a sequence of mutations become one pure operation, and the ST machinery lets you keep that sequence encapsulated. This is how building a vector works. In a new state thread, you allocate the vector. You run a pure function to generate the nth element. You bit-bang that into memory. When you're at the end of the vector, you freeze the vector into an immutable vector and return it. The final signature is "Length -> (Index -> Value) -> Vector of Values". Totally pure to the outside world, mutable internally. And the type system keeps the mutation part safe!
So while the first approach you try with Haskell should be actual pure code, you still have the tools you need to be as impure as you need to be. And you can still keep your program type safe, if you want.