Haskell series part 9
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 awhere
. - 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 functionshow
to convert ourFloat
into aString
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
andSomeException
. - 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 functiontry
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 usingevaluate
. And finally we castresult
explicitly as an IO action withEither
an exception (SomeException
is the root exception in Haskell, in your code you might want to narrow it down) or anInt
being the result of our division. - In order to handle
result
we use acase
which acts like a switch statement in other languages. Either
is an interesting type used bytry
, it simply holds two values:Left
orRight
. In the context of thetry
,Left
holds the exception, whileRight
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.