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.
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
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
modulekeyword to define all your functions between
()and follow with a
- 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
- We test both functions and print out the result through
putStrLnwhich we saw in our previous article. One thing to note is that we used the function
showto convert our
Stringso 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.
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
IOoperations, 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.Exceptionmodule in order to use:
- Second, we declare our
main. Even though the classic "Division by zero" problem is done through pure code, we will need IO.
- We use
letto 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
trywill 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
resultexplicitly as an IO action with
Eitheran exception (
SomeExceptionis the root exception in Haskell, in your code you might want to narrow it down) or an
Intbeing the result of our division.
- In order to handle
resultwe use a
casewhich acts like a switch statement in other languages.
Eitheris an interesting type used by
try, it simply holds two values:
Right. In the context of the
Leftholds the exception, while
Rightholds 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
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
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.