Well, unless by perfect world you mean telepathic computers (yikes!), then you'll require some way to process user I/O - I'll assume something like orthogonal persistence has subsumed the more boring file I/O...
Let's start with input...because it already has one solution. From page 4 of 11 in Conal Elliott's and Paul Hudak's pioneering paper Functional Reactive Animation:
lbp; rbp : Time → Event Event ( )
which, in Haskell, would look something like:
 -- read left and right mouse button-press events
lbp, rbp :: Time -> Event (Event ())
So for input from the keyboard:
kbd :: Time -> Event Char.
Other inputs can be dealt with in similar fashion.
So...what about output? The actual word doesn't appear anywhere in the paper (neither does "I/O" for that matter) - we'll have to figure this one out ourselves. But this time, it's our Haskell translation:
lbp, rbp :: Time -> Event (Event ())
providing the hint - Event () - the unit-event. That can serve as the result of sending a Char off, to appear somewhere on your screen:
viewChar :: Char -> Time -> Event ()
Again, other outputs can be dealt with using similar techniques.
...what's that - it isn't denotative?
Because viewChar is...what - impure?
If so, that means lbp and rbp are also impure - are you sure about this?
Alright...let's have a type for taking in a series of those mouse button-press, or other events:
type Intake a = [a]
lpb, rbp :: Intake (Event (Event ())
Is that any better? Good! Well, sort of -
what happens if the mouse is unplugged? That
could put parts of a program into a spin waiting for input (and using [] would permanently end the series - no more button presses!).
We need to change Intake:
data Intake a = None (Intake a) | Next a (Intake a) 
Now unplugging the mouse results in None …
appearing, which a program can detect and react accordingly e.g. yielding its OS thread,
suspending itself, etc.
So, what about output? Well, output devices can often be unplugged too. Taking a hint from Intake:
data Outlet a = Wait (Outlet a) | Went (… (Outlet a) …) 
It's similar to unplugging an input device - upon encountering
Wait …, a program can pause transmission.
So what should the type of Went be? Well, an Outlet accepts values incrementally to allow Wait … to appear if needed - the accepting of each value should present us with the rest of the Output. Therefore:
data Outlet a = Wait (Outlet a) | Went (a -> Outlet a)
Bringing that altogether:
data Intake a = None (Intake a) | Next a (Intake a)
lbp, rbp :: Intake (Event (Event ())
data Outlet a = Wait (Outlet a) | Went (a -> Outlet a)
viewChar :: Outlet Char
So is all this valid? If you're not sure, see section 20.4.2 (page 86 of 263) of Fudgets - Purely Functional Processes with applications to Graphical User Interfaces by Magnus Carlsson and Thomas Hallgren - if Intake and Outlet look dubious then so is what can be seen there, in the paper...