Road to Haskeller #12 - Hello, World! (IO Actions)

Last Edited: 6/25/2024

The blog post introduces an important concept, IO actions, in Haskell.

Haskell & Hello World

Hello, World!

Finally, we are ready to talk about how to print "Hello, World!" in Haskell. Let's dive straight into the code.

main :: IO ()
main = putStrLn "Hello, World!"

Let's break it down and understand each component. First, we use the name main as a naming convention that allows GHC to recognize that it will havr to run this function. Hence, if we use GHC instead of GHCi by using ghc --make hello-world.hs for compilation and ./hello-world for execution, the main function will get executed. (Alternatively, we can use runhaskell hello-world.hs to execute the program on the fly.)

We can see that the type assignment for the main function is IO (). IO refers to an IO (Input Output) action, which can interact with the environment (like the keyboard and screen) while mapping input to output. () refers to the output type of this IO action, which is an empty tuple indicating no output. Now, some of you might have noticed that it breaks the purity of functions since IO actions do have side effects. You are right. There are limits to how pure you can go because if we make it fully pure, we would not even be able to see the result from executing a function. IO actions are set up so that they can interact with the environment while maintaining as much purity as possible.

putStrLn is an IO action (with type IO ()) for printing out a string. Its input must be a string, so putStrLn 3 will emit an error. We have the print function that can print out a value of any type in the Show typeclass, which is made by putStrLn . show. However, we often prefer using putStrLn instead of print for strings because print will attach "" to strings. In fact, GHCi uses print implicitly for displaying the output of a function.

Well, that was a lot of explanation for a program that just prints out "Hello, World!", but understanding individual components will be extremely helpful in dealing with IO actions in the future.

Do and Return

We can use do notation to group multiple IO actions into one IO action.

main = do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn "Hello, " ++ name ++ "!"

Let's understand what name <- getLine is doing. getLine is an IO action with type IO String that takes user input and outputs a string. We can use <- notation to bind the result to the variable name, so that the resulting string can be used later. Some programmers are confused about getLine and do:

main = do
  putStrLn "What is your name?"
  putStrLn "Hello, " ++ getLine ++ "!"

However, the type of getLine is IO String and not String. We need to get access to the output of getLine with <- notation. As IO actions are functions that interact with the environment, we can use IO actions recursively.

main = do 
    putStrLn "What is your name?"
    name <- getLine
    if null name
        then return ()
        else do
            putStrLn $ "Hello, " ++ name ++ "!"
            main

In the above IO action, we retrieve user input and print out "Hello, name!". However, instead of ending the execution, it repeats the execution until the user input is null. This is achieved by having an if statement and using main inside of main. We also see that do is used inside of else. This is because each case needs to have exactly one IO action for the whole main to be an IO action, and we need to group 2 IO actions putStrLn and main into one.

How about return ()? As already mentioned, each case needs to have exactly one IO action. However, we do not want to do anything when a name is not provided by the user. Hence, we use an IO action return () to do nothing. The () part indicates that it outputs nothing or (). We can think of return as the opposite of <- in the sense that <- binds a result of an IO action while return creates an IO action with a particular result. This means the following is valid:

main = do
  greeting <- return "Hello, "
  name <- return "Alice!"
  putStrLn greeting ++ name

In the above, we are using return to create IO actions that do nothing but produce outputs "Hello" and "Alice!" and we are binding the outputs of those IO actions to greeting and name with <-. However, it is redundant as we can use let in the do block.

main = do
  let greeting = "Hello, "
  let name = "Alice!"
  putStrLn $ greeting ++ name

Exercises

This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.

Resources