Thank you for joining us for the tenth and last part of our Haskell series, you will find the previous article here where I explain modules and exceptions.

In this article we are going to cover Maybe, Just and Nothing as an alternative to handling exceptions and errors.

Data Type

But before jumping into the usage of Maybe, let's have a look at how we can create our own data types. We are going to rewrite the module we wrote previously for currency conversion:

-- This is CurrencyConverterB.hs

module CurrencyConverterB (
    CurrencyRate (Rate),
    convert
) where

data CurrencyRate = Rate String String Float

convert :: CurrencyRate -> Float -> Float
convert (Rate _ _ rate) amount = amount * rate
-- This is Main.hs
import CurrencyConverterB

eurToUsd :: CurrencyRate
eurToUsd = Rate "eur" "usd" 1.12

usdToEur :: CurrencyRate
usdToEur = Rate "usd" "eur" 0.88

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

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

Ok let's go step by step, first the CurrencyConvertB file:

  • Just like we saw previously, we use the module keyword to define the name of our module and what we want to export out of it.
  • You will note one subtlety with CurrencyRate (Rate) in the module, this is because of our data type which we created called CurrencyRate. Rate is the name of the constructor, sometimes developers use the same name for both type and constructor. In order to clarify the difference here I used different names.
  • Now our type CurrencyRate is composed of three things: a String, another String and a Float so that we can keep the base currency, the target currency and the exchange rate.
  • Finally we create a simple convert method which takes a CurrencyRate and an amount. Then it will simply multiply this amount with the exchange rate. You will note that when using data to create new types, the only way to access fields is through pattern matching. I recommend that you look into "records" to make this part easier, especially if you need more fields.

Now that we understand this module, let's have a look at our new Main:

  • First we import our module CurrencyConvertB.
  • Next we define two functions which uses our Rate constructor to create new CurrencyRate. The syntax is pretty simple: ConstructorName field field.
  • Then we call our convert method using the rates we defined above and get the same results as our previous code.

Let's run it in GHCI:

ghci> :l Main
[1 of 2] Compiling CurrencyConverterB ( CurrencyConverterB.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

Neat, everything gets automatically compiled based on the Main's imports. Note that you can also compile it and run it like so:

➜  ghc --make Main.hs
[1 of 2] Compiling CurrencyConverterB ( CurrencyConverterB.hs, CurrencyConverterB.o )
[2 of 2] Compiling Main             ( Main.hs, Main.o )
Linking Main ...
➜  ./Main 
10 EUR = 11.2 USD
10 USD = 8.8 EUR

Maybe

Now, to answer the question: "Why did we learn about data types before Maybe ?"

It is because Maybe would probably look something like this:

data Maybe a = Just a | Nothing

A few things to note:

  • The definition of Maybe is parameterized with a type variable a so we can use it with any type we want.
  • Maybe has two possible constructor: Just a or Nothing . When creating a new data type you may add different constructors and separate them with a pipe |.

The idea behind Maybe is mainly to check for the presence of a value, if you want to compare it to some other languages it is similar to: "This function returns a user or null". So translated to Haskell it would be: "My function returns Just a user or Nothing".

Let's look at some simple code to understand of usage of Maybe:

-- this is maybe.hs
import Data.List

main = do
    let a = [1, 2, 3, 4, 5]
    let result = find (<5) a
    putStrLn (show result)

This is a very typical example, we are loading the standard module Data.List to use the find function. I copied the definition below:

The find function takes a predicate and a structure and returns the leftmost element of the structure matching the predicate, or Nothing if there is no such element.

Let's verify this:

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

Indeed, we have Just the first element of the list, being 1. Now let's change the code from <5 to >5 and recompile:

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

This time we get Nothing.

Now, let's try to programmatically handle both cases, we will add a few lines to our code in maybe.hs:

-- this is maybe.hs
import Data.List

main = do
    let a = [1, 2, 3, 4, 5]
    let result = find (<5) a
    case result of
        Nothing -> putStrLn "Sorry, could not find what you were looking for"
        Just b -> putStrLn ("Found your " ++ show b)

We used the same method with a case statement as in our try example in our previous article. Now let's recompile and run it:

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

Let's change again the < sign:

ghci> :l maybe.hs 
[1 of 1] Compiling Main             ( maybe.hs, interpreted )
Ok, one module loaded.
ghci> main
Sorry, could not find what you were looking for

Great, everything works as expected !

Conclusion

Ten down ! It took me more than 6 months but we are finally there, 10 articles to introduce important concepts of Haskell to get beginners started in this hidden gem of a language. There are still many concepts to discover such as: Functor, Monoid, Monad and so on. If you want to continue your Haskell experience, I encourage you to subscribe to Haskell Weekly newsletter to get interesting content every week. Until then, good luck in your learning adventure !

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