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

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
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #15 - IO. YouTube.
- NA. Input and Output. Learn You a Haskell for Great Good.