Input & Output (IO) – Haskell

In Haskell, input and output (IO) work a bit differently than in imperative languages. Since Haskell is a purely functional language, functions are expected to be pure, meaning they should produce the same output given the same input and have no side effects. However, input and output inherently involve side effects, which poses a unique challenge. To manage this, Haskell uses the IO type to separate pure code from code that performs IO.

This article covers the basics of Haskell’s IO system, explaining how you can read input, write output, and work with files.

1. The IO Type

In Haskell, all functions that perform IO return a type marked with IO. For example:

  • getLine :: IO String — a function that reads a line of input from the user, producing an IO String.
  • putStrLn :: String -> IO () — a function that prints a line of text, taking a String and producing an IO ().

The IO type indicates that a function has side effects. Importantly, IO actions can’t be used directly within pure code; they need to be managed within an IO context, typically using the main function.

2. Basic Input and Output Functions

Here are some commonly used functions for basic IO in Haskell:

Output Functions

putStrLn: Prints a string with a newline at the end.

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

putStr: Similar to putStrLn, but it doesn’t add a newline.

main :: IO ()
main = do
    putStr "Hello, "
    putStr "Haskell!"

Input Functions

getLine: Reads a line of input from the user as a String.

main :: IO ()
main = do
    putStrLn "What's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name ++ "!")

In this example:

  • name <- getLine reads input and binds it to name.
  • putStrLn then prints a greeting, appending the user’s name.

getChar: Reads a single character.

main :: IO ()
main = do
    putStrLn "Press any key to continue..."
    char <- getChar
    putStrLn ("You pressed: " ++ [char])

3. The do Notation

To perform a sequence of IO actions, Haskell provides the do notation. This allows you to write imperative-style code while still working within a functional framework. Here’s how it works:

main :: IO ()
main = do
    putStrLn "Enter your age:"
    age <- getLine
    putStrLn ("You are " ++ age ++ " years old!")

In this example:

  • Each line in the do block is an IO action.
  • <- is used to bind the result of an IO action (getLine in this case) to a variable (age).

4. Working with Pure and IO Values

A common challenge in Haskell is using pure functions with IO values. For instance, after reading user input, you may want to process it with a pure function. To do this, you can use let to define a pure value within a do block:

main :: IO ()
main = do
    putStrLn "Enter a number:"
    input <- getLine
    let number = read input :: Int
    putStrLn ("The square of your number is: " ++ show (number * number))

Here:

  • input is an IO String, but let binds number as a pure Int after parsing input.
  • read converts a String to another type (like Int), and show converts a value back to String for display.

5. File IO

Haskell also provides functions for reading from and writing to files.

Reading from a File

readFile: Reads the contents of a file as a String.

main :: IO ()
main = do
    contents <- readFile "example.txt"
    putStrLn "File Contents:"
    putStrLn contents

Writing to a File

writeFile: Writes a String to a file, overwriting the file if it exists.

main :: IO ()
main = do
    let content = "This is a line of text."
    writeFile "output.txt" content
    putStrLn "File written."

appendFile: Appends text to an existing file without overwriting it.

main :: IO ()
main = do
    appendFile "output.txt" "Appending some text.\n"
    putStrLn "Text appended to file."

6. Combining IO and Pure Functions

In Haskell, you often need to process data with pure functions after reading it from IO. Here’s a simple example:

doubleNumber :: Int -> Int
doubleNumber x = x * 2

main :: IO ()
main = do
    putStrLn "Enter a number:"
    input <- getLine
    let number = read input :: Int
    let doubled = doubleNumber number
    putStrLn ("Double of your number is: " ++ show doubled)

In this example:

  • doubleNumber is a pure function.
  • After reading the input and parsing it into an Int, we pass it to doubleNumber and print the result.

7. Using return in Haskell

In Haskell, return does not cause early exit as it might in imperative languages. Instead, it wraps a pure value in an IO type. Here’s a simple example to demonstrate:

main :: IO ()
main = do
    let value = 42
    wrappedValue <- return value
    putStrLn ("Wrapped value is: " ++ show wrappedValue)

In this case, return value doesn’t perform any action—it just produces an IO value of 42 that can be used within the do block.

Summary

Haskell’s approach to input and output is different from imperative languages, using the IO type to separate pure and impure code. Here are the key points:

  • The IO Type: All functions that perform IO return an IO type, distinguishing them from pure functions.
  • Basic IO Functions: putStrLn, getLine, and other functions allow basic interaction with the console.
  • do Notation: Provides a way to sequence IO actions in a readable way.
  • File IO: Haskell offers functions like readFile, writeFile, and appendFile for working with files.
  • Combining IO and Pure Code: You can use pure functions within IO code by reading and processing values.

By understanding Haskell’s IO system, you can manage side effects in a controlled way, making your code easier to reason about while still being able to interact with the outside world. This separation between pure and impure code is one of the key features that makes Haskell unique.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *