If your brain can encompass the thought, yes, it is arguably the best and most correct way to think of a Haskell program. This is why Haskell programmers can claim with a straight face that "main" is actually a pure value of type IO and the language has no impurity, and there is an important sense in which this is completely true and not the cop out some people think it is in the slightest.

The laziness allows the pure language constructs like "if" to be lifted up into the IO value. That is, if you embed an if statement into one of the (pure) functions used in an IO value somewhere, only the branch that the "if" would take is ever evaluated. However, technically speaking, you can also look at the branches happening for all inputs; if you input a true or a false from the user, you can look at that as a true branch, a false branch, and some additional error branches, even if the code doesn't look like it. A strict language would not permit this. (Though even in a strict language, real-world useful IO values end up with enough function calls in them that a strict language wouldn't manifest the full tree either.)

If you input a full Int32, you can look at that as defining 2^32 possible branches in the IO value, and the execution will take one of them. In reality, the program does not do 2^32 different things, and a better view of what the program is doing is the more conventional one for almost every purpose. (Although that view will still pass through a lot of possible states!) But in terms of understanding how the IO value is "pure", this view is momentarily helpful.

The only operation in a Haskell program that violates this is unsafePerformIO, which really does penetrate this abstraction and work like a "normal" program. Otherwise, no matter how much Haskell code you write, technically all you're doing is making a bigger and more complicated pure IO value, which defines how to have an effect on the world but does not itself have an effect on the world. Executing the program is what finally puts the two together.

To put it another way, Haskell has a completely clear separation between being a program and executing a program. If I say to someone, "pick up that glass, fill it with water, and water this plant", that statement is itself a "pure value". The execution of that statement is where things get impure. Some languages do not have this clear separation, most notably Perl but the dynamic languages in general don't. Many others do, as having a compilation step all but forces this sort of separation, they just don't think of it that way and there can be leaks here and there, and features that may blur the line deliberately. Haskell does have a very clear separation, and in that separation, with laziness, it is almost like IO is just one big macro language for putting together programs, in some sense beyond what even Lisp would dream of.

And in another sense it's just a funny way to write conventional programs with really weird pretensions, and there's a certain value to that point of view too, which is that when you're done blissing out on the hippie math juice, when you actually sit down and write code in Haskell you're doing much the same thing you do in any other language and treat IO just like any other source code. But, as with the story of enlightenment... "first it was a mountain, then it was not a mountain, then it was a mountain again"... where you end up on this journey is not quite the same as where you started.