April 28, 2025

Haskell in 2025 - IO and "hello world"

IO in Haskell is surprisingly complex and sophisticated. In other words, it is filled with unexpected surprises.

Many books postpone talking about it by having you use ghci to play with Haskell. This is procrastination by any other name. There are good reasons for this -- that I do not embrace. Here is the one line "hello world" program.

main = putStrLn "Hello World!"
The first thing to know is that IO in Haskell takes place when you assign a value to the function "main". This is special and unique magic that applies only to main.

The function "putStrLn" takes a string and wraps it into what is called an IO action. We can now do a careful dance with words. There is a very important difference between defining a function which is an IO action and triggering that action that is latent in that function! This is why the phrase "IO action" is used.
Consider the following:

hello = putStrLn "Hello World!"
main = hello
Here we define a function "hello" that is an IO action that will always print "Hello World!". No IO takes place when we define this function, and at this point this is nice pure code. The IO action is triggered only when we assign it to main.

A quick discussion about purity

Just saying that a language is "pure" sounds good, whatever it means. What it means in the context of Haskell is that if a function is pure, it has no side effects. Of course we need to define "side effects", but most experienced programmers will have a good idea at this point. Discussions of purity that try to get rigorous get mired in talk about "referential transparency" and set theory. This kind of talk doesn't explain anything, but just substitutes other words, which doesn't clarify anything if they don't already mean something to you.

Drag "do" into the game

So if you want to say hello several times you can use:
hello = putStrLn "Hello World!"
main = do
    hello
    hello
    hello
We can (and will) talk about "do" at a later time. One way to describe it, that ignores all the details, is that it is a handy way to trigger several IO actions one after the other when you only have one "main"! The be a bit more truthful, do introduces a sort of "sub-language" within Haskell with its own special rules. Yet more truthful (but more interesting) is to admit that do deals with monads. IO actions are a particular kind of monad, and "do" notation lets you chain monads together. This should make little or no sense at this point if you are new to Haskell, but does let you know you are only seeing the tip of an iceberg when you use do notation along with main and IO actions.

A quick look at "interact"

Here is another example from the end of chapter 1 in "Real World Haskell"

main = interact wordCount
	where wordCount input = show ( length(lines input)) ++ "\n"
interact is the interesting Haskell library function here. It is an IO action (or else we could not assign it to main). It takes a function as an argument. The function must take a string as input and product a string as output. Interact will read stdin and feed that as input to the function, then send the output to stdout. We can ditch the "where" construction and write the above like this:
wordCount input = show ( length(lines input)) ++ "\n"
main = interact wordCount
Here wordCount is a pure function and not an IO action at all. The "interact" function is just a handy way of constructing filters in Haskell.
Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org