Therefore, say, querying database or writing file could not be done in a pure functional style by definition. That's for example, is one of reasons we need monads.
Nobody "needs" monads, that's just one way to describe things. In fact, it's probably not even the best way. Some form of effect typing, uniqueness types, or a system based on full linear logic seem more persuasive in theory but are all more radical departures from well-known type systems and more complicated to express. Monadic IO as found in Haskell is a compromise between usability and simplicity, since it essentially models fully imperative programming in a way that coexisted easily with the existing ML-style type system already used in the language.
The question is - why we consider STDOUT output as something impure? Yes, any filehandler is risky - we never can be sure that data always will be written. But what about STDOUT? Why should we think of it as of something unreliable? Is it more unreliable that evaluation itself? I mean, we always can pull the trigger and thus, interrupt calculation.
It's not, and we don't. Input to, and output from, the program as a whole can be simply regarded as arguments and results from treating the entire program as one large pure function. As long as it prints the same thing to stdout if you feed it the same thing from stdin, it's still a pure function. In fact, before introducing monadic IO, Haskell used a stream-based I/O system that used pure lazy streams for input and output. It dropped it because it was apparently a pain to use, which may give you some idea of why you haven't heard of anything like this. :]
To make the point in a sillier way, consider the minimalist esoteric language, Lazy K:
Lazy K is a garbage-collected, referentially transparent functional programming language, with a simple stream-based I/O system.
What distinguishes Lazy K from other such languages is its almost total lack of other features. It does not, for example, offer an integrated Hindley-Milner polymorphic type system. It is not shipped with an extensive standard library with support for platform-independent GUI programming and bindings to other languages. Nor could any such library be written since, among other things, Lazy K does not provide any way to define or refer to any functions other than built-ins. This inability is complemented by a matching lack of support for numbers, strings, or any other data type. Nevertheless, Lazy K is Turing-complete.
Lazy K programs live in the same timeless Platonic realm as mathematical functions, what the Unlambda page calls "the blessed realm of the pure untyped lambda calculus." Just as garbage collection hides the process of memory management from the programmer, so referential transparency hides the process of evaluation. The fact that some calculation is necessary in order to view a picture of the Mandelbrot set, or in order to "run" a Lazy K program, is an implementation detail. That's the essence of functional programming.
How to handle input and output in a language without side effects? In a certain sense, input and output aren't side effects; they are, so to speak, front- and back-effects. So it is in Lazy K, where a program is simply treated as a function from the space of possible inputs to the space of possible outputs.
I doubt you'll find a more purely functional language than that!
Keep in mind, though, that the above applies only to essentially taking the input and output of a pure function and connecting them to stdin/stdout "externally" in some way. There's a big difference between that and having access to the real system-level I/O primitives. The implementation details of reading and writing to the streams may leak impurity unless carefully encapsulated.
I expect this is the main reason that you can't do this directly in Haskell--the sensible use cases are slim compared to using monadic IO, and for the latter there's a lot of benefit to having access to the real thing. I believe that's why, for instance, the command-line arguments to the program aren't simply passed as arguments to
main, even though it seems intuitively that they should be.
You can recover a minimal version of something like this in a specific program, though--just capture the arguments as pure values and then use the
interact function for the rest of your program.