Disclaimer: I’m far from being an expert here, this is the mental image that I’ve built, which might be completely wrong.
What is really in place isn’t some magic of getContents
. It’s general magic of Haskell’s laziness. contents
isn’t a string. It’s an unevaluated expression which (when evaluated) will result in a character and another unevaluated expression and so on. Finally, one of those expressions will evaluate to an empty list (remember, strings are just lists of characters). This has nothing to do with IO (as in operation, not the type IO), the IO is hidden in what it takes to evaluate that expression.
Now, you go to the second line. putStr
needs a whole list, but it’ll also take it one character after another. So, it’ll ‘ask’ it’s argument (a synonym which I’ll use for ‘evaluate’) which happens to be map toUpper contents
. At this point map
asks contents
‘give me the next element of the list’ (so that it can put it through toUpper
and return to putStr
). At this points contents
(which is an unevaluated return value of getContents
) figures out it doesn’t have any character to return, so it reads some of it from the operating system. How many? Some. As much as OS is willing to give at that specific moment, maybe one, maybe 1000 (if you’ve redirected stdin to read a book), and as much as fits in it’s internal buffers which are specified, but hidden somewhere deep. This is all it takes to print the first letter of response. Assume it’s 10 for further discussion.
Now putStr
asks for the second letter. The procedure starts the same, but contents
happily serves the next letter from it’s internal buffers. Some more iterations pass and this time the buffer is (again) empty. contents
will reach out to OS to get the next few characters. At some point it’ll get from OS information that the file has ended, and it’ll return an empty list instead of next character.
It doesn’t matter here that much, as the string is sufficently small. But, if you pipe a several GB worth of text through it, it’ll still continue this ‘reading in chunks’ workflow instead of gulping all of that into RAM (possibly exhausting it) and processing everything in-memory before spitting out to stdout.
And if you wonder, putStr
also does it like this - it won’t wait for the whole string to give it to OS, it’ll happily feed OS one fragment at a time, whose fragments being either single charaters or some small internal buffers it fills one-character-at-a-time.