Haskell series part 8

This is the eighth article of a series on the functional language Haskell for beginners

Haskell series part 8

Thank you for joining us for the eighth part of our Haskell series, you will find the previous article here where I explain map, filter, foldl as well as function composition.

In this article we are going to cover IO (Input and Output) in Haskell.

Side effects

In our first article we took a definition of Haskell from Wikipedia and noted that Haskell is "pure" (among other things). The main idea behind this is that if you run a line of code with the same parameters once or 1,000 times it will always have the same results and no (observable) side effects.

Now the question is: how does Haskell keeps the language pure especially when you need to interact with "the outside world", like connecting to server, writing in a file, or just a simple print ? The answer is: IO actions.

Speaking of print, let's check the Haskell equivalent which also adds a new line at the end:

ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> putStrLn "test"
test

So putStrLn takes a string as a parameter and returns an IO action (for now, let's think of it as something to be done which will yield something once it is done) with an empty tuple (). It makes sense, we do not expect any useful value to be returned from a print.

Let's try another one:

ghci> :t getLine
getLine :: IO String
ghci> getLine
test
"test"

getLine waits for the user to input something from the terminal. From the definition we can see that it takes no parameter but returns an IO action with a string.

>> and >>=

Usually when you have to resort to IO (in any language), it is because you need to have some sort of sequential computation. Let's try a simple example: we will write a program which asks a user for their name.

ghci> putStrLn "Hi, what is your name ?" >> getLine
Hi, what is your name ?
Pierre
"Pierre"

Using >> we can chain IO actions together, but there is one problem: the result of every action is dropped before performing the next one. Which could be fine in most cases, but in our program we want to say "Hi" back to the user using the name we received in getLine. Luckily there is another primitive we can use for this:

ghci> putStrLn "Hi, what is your name ?" >> getLine >>= \name -> putStrLn("Hi " ++ name)
Hi, what is your name ?
Pierre
Hi Pierre

You will note that on the third IO action of the chain we used a lambda function with name as a parameter, which is the direct output of the previous IO action.

Do block

Now, even though you could break down those one liners into multiple lines, using this sort of syntax every time you need to have a sequential computation could be problematic. Luckily we have the do notation ! Let's try to rewrite the last example using it. We are not going to use GHCI this time, we are going to write code in a file:

-- This goes into io.hs
main = do
    putStrLn "Hi, what is your name ?"
    name <- getLine
    putStrLn("Hi " ++ name)

As you can see, this sort of syntax is much closer to "good old imperative languages" where you define a sequence of actions to be done step by step inside a main.

You might be wondering why we must use <- instead of simply a = when getting the name from the user. Well it is because you need to perform an IO action before getting its result, using the "reverse arrow" will perform the action and then bind the result to the variable called name.

If you would have used =, you would have just created a new variable to hold getLine:

ghci> name = getLine
ghci> :t name
name :: IO String
ghci> name 
test
"test"

Now that we cleared all of this, we can load it in GHCI (or compile it, as you want) like so:

ghci> :l io.hs
[1 of 1] Compiling Main             ( io.hs, interpreted )
Ok, one module loaded.
ghci> main
Hi, what is your name ?
Pierre
Hi Pierre

Looks good.

Files

Now that we have a basic understanding of IO, let's have a quick introduction on how to read and write files in Haskell. It is actually pretty simple. For the following examples we are going to create a file "lorem.txt" and shove the usual Lorem Ipsum inside:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
-- This goes in read.hs
main = do
    text <- readFile "lorem.txt"
    putStrLn text

We simply need to make a call to the IO action readFile to read the whole content of the file and bind it to a variable we called text:

ghci> :l read.hs
[1 of 1] Compiling Main             ( read.hs, interpreted )
Ok, one module loaded.
ghci> main
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

To write to a file (and override its content entirely) there is another IO action writeFile:

-- This goes in write.hs
main = do
    let text = "I wish I knew latin"
    writeFile "lorem.txt" text

Which we proceed to load into GHCI:

ghci> :l write.hs
[1 of 1] Compiling Main             ( write.hs, interpreted )
Ok, one module loaded.
ghci> main
ghci>

Then we can verify what happened to our file:

➜  test-haskell more lorem.txt 
I wish I knew latin

Nice, the content of the file has changed with the content of our variable text. For those of you wondering about it, there is an IO action appendFile in case you want to add content at the end of the file (it would be the equivalent of the mode a for the pythonists out there).

Conclusion

Eight down, around two more articles to go ! That's it for IO in Haskell.  In our next article we are going to discuss modules and exceptions which will help us structure our code better.

PS: Part 9 can be found here.

If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.