Thank you for joining us for the ninth part of our Haskell series, you will find the previous article here where I explain IO.

In this article we are going to cover modules and exceptions.

Imports

Just like most languages, Haskell is divided in "modules". A module is a collection of functions and types and so on. And in order to use a specific module, you need to import it. So far we did not import anything as whatever we needed was already loaded by default.

A great start to checkout Haskell libraries is to go there.

First let's have a look at the Data.Char module, it contains a lot of character related functions, let's try to use some of them in GHCI:

ghci> :m Data.Char
ghci> isLower 'a'
True
ghci> isUpper 'a'
False

You simply need to use :m <module name> to load a module in GHCI, and then you can directly call its functions.

Let's do the same inside a file now:

-- This goes in module.hs
import Data.Char

isItLower :: Char -> Bool
isItLower x = isLower x

Which we load in GHCI:

ghci> :l module.hs 
[1 of 1] Compiling Main             ( module.hs, interpreted )
ghci> isItLower 'a'
True
ghci> isItLower 'A'
False

You might say "I am actually used to import things into a namespace or an alias", well fear not, as it is possible like so:

-- This is our new module.hs
import Data.Char as C

isItLower :: Char -> Bool
isItLower x = C.isLower x

Now the content of the Data.Char module is accessible through C.

Creating your own modules

Now that we figured out how to import modules, let's try to make our own. We are going to write a simple Haskell module which converts currencies:

-- This is CurrencyConverter.hs

module CurrencyConverter (
    usdToEur,
    eurToUsd
) where

eurToUsd :: Float -> Float
eurToUsd eur = eur * 1.12

usdToEur :: Float -> Float
usdToEur usd = usd * 0.88

It is pretty straightforward:

  • You need to use the module keyword to define all your functions between () and follow with a where.
  • You can define your function signatures and bodies right after. Everything between the () will be "exported" and the rest will be "ignored".

Now let's write a simple main to import and use our module:

-- This is Main.hs
import CurrencyConverter as CC

main = do
    let eur = CC.eurToUsd 10
    putStrLn ("10 EUR = " ++ show eur ++ " USD")


    let usd = CC.usdToEur 10
    putStrLn ("10 USD = " ++ show usd ++ " EUR")

Nothing surprising here:

  • We import our module and make it accessible through CC.
  • We test both functions and print out the result through putStrLn which we saw in our previous article. One thing to note is that we used the function show to convert our Float into a String so we can concatenate it into a sentence.

Now let's try it out in GHCI:

ghci> :l Main.hs 
[1 of 2] Compiling CurrencyConverter ( CurrencyConverter.hs, interpreted )
[2 of 2] Compiling Main             ( Main.hs, interpreted )
Ok, two modules loaded.
ghci> main
10 EUR = 11.2 USD
10 USD = 8.8 EUR

Automatically, GHCI compiled the import module and then the main file for us.

Exceptions

Haskell just like other languages gives us tools to handle exceptions during the runtime of our program. Most of it is inside the module Control.Exception which can be found here.

If you followed the link above you would have found this quote:

In addition to exceptions thrown by IO operations, exceptions may be thrown by pure code (imprecise exceptions) or by external events (asynchronous exceptions)

Let's try to explain it:

  • IO operations: This is for instance when reading a file (as seen in our previous article) and the file cannot be found on the disk.
  • Pure code: This is for instance when trying to divide a number by 0, we are not doing anything IO related.
  • Asynchronous events: This is when an exception arises from a different thread, it is a bit too complex so we will skip it for now.

Try and Either

This is an example of a pure code exceptions being thrown:

ghci> x = 100 `div` 0
ghci> x
*** Exception: divide by zero

Let's write some code to try to run this line and catch an exception if it occurs:

-- This goes into exceptions.hs
import Control.Exception

main = do
    let x = 100 `div` 0
    result <- try (evaluate (x)) :: IO (Either SomeException Int)
    case result of
        Left exception  -> putStrLn (show exception)
        Right value -> putStrLn (show value)

So there is a lot to unpack here:

  • First, we import our Control.Exception module in order to use: try, evaluate and SomeException.
  • Second, we declare our main. Even though the classic "Division by zero" problem is done through pure code, we will need IO.
  • We use let to hold our division by zero, note that it is evaluated lazily, nothing will blow up on this line yet.
  • Then we use the reverse arrow to execute an IO action and bind it to result. The function try will execute the code and let us know if something went wrong. Here, because it is a "pure code" exceptions we need to force it's evaluation by using evaluate. And finally we cast result explicitly as an IO action with Either an exception ( SomeException is the root exception in Haskell, in your code you might want to narrow it down) or an Int being the result of our division.
  • In order to handle result we use a case which acts like a switch statement in other languages.
  • Either is an interesting type used by try, it simply holds two values: Left or Right. In the context of the try, Left holds the exception, while Right holds the actual value (if the computation in the try went through).

Let's run it:

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

Looks good, we show the Left part of Either. And now if we modify our 0 by 5:

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

Now we have the Right part.

Conclusion

Nine down, only one more article to go ! That's it for modules and exceptions in Haskell. While try is a standard way to handle exceptions, most of the time developers avoid using IO in Haskell to keep the purity to the maximum. There are other ways to handle exceptions such as using data types with Maybe, Just and Nothing which we will cover in our next article.

PS: Part 10 can be found here.

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