It's worth noting that things like print and random are just as much side effects as mutability. To eliminate random is easy: realize that random generation is a function from a hidden random generator state to a "random" value and a new random generator state. Now use state-minimization techniques.
Print alone is easy as you can just have functions output both their normal output and a listing of whatever they printed. These listings then get concatenated together (this is technically "the writer monad", but you don't need to know as much).
IO was originally handled in Haskell as `main :: [String] -> [String]`. More generally, we might think `main :: [Input] -> [Command]`. This is obviously a pure function. If the types Input and Command are a bit like
To eliminate print is much harder.