> It's one of the few truly purely functional languages around. No IO monads or whatsit.
The IO monad, as in Haskell, can be viewed as an entirely pure construct. Its implementation is impure purely because of performance reasons. You can write your own completely pure IO type if you want, with unchanged semantics.
A basic example of this would be
data IOF r = PutStrLn String r
| GetLine (String -> r) deriving Functor
type IO = Free IOF
putStrLn s = liftF (PutStrLn s ())
getLine = liftF (GetLine id)
main :: IO ()
main = do
putStrLn "Enter your age"
age <- read <$> getLine
if age >= 18
then putStrLn "You can vote!"
else putStrLn $ unwords ["You can vote in", show (18-age), "years"]
Of course, now you need a way to interpret your IO type in order to make the machine actually do stuff. This can be achieved in two ways: You can modify the GHC RTS to allow it to interpret your IO type, or you can define an interpreter in Haskell to convert it to the IO type that the GHC RTS knows how to interpret interpret :: IO a -> Prelude.IO a
interpret (Free (PutStrLn s r)) = Prelude.putStrLn s >> interpret r
interpret (Free (GetLine s)) = Prelude.getLine >>= interpret . s