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