If you don't have exceptions and you don't have null
: How do you handle errors and invalid inputs? Based on your background, this can either be trivial or mind-bending. Coming into the Haskell (actually: Elm and Rust) world from C#, JavaScript, and Python, it definitely wasn't obvious to me at first.
Luckily, the Maybe
and Either
types are there for you, and they are fairly easy to get started with. In this post, I want to give a quick introduction to both of these data types and how they're used.
To any returning readers: This post is based chapter 12 of /Haskell Programming from First Principles/, which deals with basic error handling in Haskell, using the ~Maybe~ and ~Either~ types. The chapter doesn't actually introduce much else of interest, so I wanted to approach this entry slightly differently.
What we will (and won't) be covering
This post is intended to be a brief introduction to the Maybe
and Either
data types in Haskell; suitable for someone who has no previous experience with these. Some understanding of Haskell syntax would be beneficial, but is not required. The post should give the reader a basic understanding of what Maybe
and Either
are and how they can be used for modeling data and responses.
This post is not intended to be a thorough examination of these types. It will not discuss anything related to typeclasses, mapping, or binding. There will also not be any extensive code samples. As such, please note that there is a lot more to these data types than we will look at here, but that most of it is out of scope for this post.
For further reading, please consult the section at the end of the article.
Maybe
Maybe
represents the potential absence of a value. It is used when some data can be in one of two states: defined/present, or undefined/absent. Because Maybe
represents a value that may or may not be present, we also need to specify the type of the potentially contained item: Maybe String
, Maybe Integer
, or Maybe a
for instance.
In many ways, Maybe
is similar to null
in C#, JavaScript, etc., None
in Python, and related concepts (like nil
) in a lot of other languages. However, in these other languages, the potential lack of a value is (almost) always implicit, meaning the programmer can never be sure whether the value is there or not. In Haskell, this is always explicit. The Maybe
type is the same as the Option
type in languages like Rust and F#.
The data declaration looks a little something like this:
data Maybe a = Nothing | Just a
As such, Maybe
has two data constructors, Nothing
and Just
, where the latter takes a value to wrap.
Basic usage
While there's any number of ways you can use the Maybe
type in your programs, I find the two most obvious ways to be
- to model data where some parts are optional
- to return something from a function if the input is invalid
Being able to indicate that a value may or may not be present is a simple, but very powerful tool of modeling data. Imagine you've created a dating app for Haskellers---let's call it Hinder---where you want to display a list of users. The list should display only their profile picture and their name, but there's no requirement to have set a profile picture to be listed, so we'll need to account for that. A simplistic way to model that would be by using a list of records like this:
data User = User
{ name :: String
, picture :: Maybe String
}
Where the picture
value is Nothing
if there is no profile picture set. If they have a profile picture, it's Just <picture id>
.
The other way to use Maybe
is as the return value of a function. As much as possible, you should use the type system to constrain the set of allowed arguments to a function, but that'll only get you so far. The built-in div
function throws an exception if you pass 0
as the second argument, so let's fix that by using Maybe
. If the divisor is 0
, return Nothing
. Otherwise, return Just <result>
.
safeDiv :: Integral a => a -> a -> Maybe a
safeDiv _ 0 = Nothing
safeDiv x y = Just $ x `div` y
Either
The Either
type is similar to the Maybe
type, but comes with some added functionality. It's also used to model things that can fail, but it provides a way to report what went wrong, not just that something failed.
Either
represents a piece of data that can be one of two things. That is, it can be either 'this' or 'that', whatever 'this' and 'that' may be. Either
is parameterized by two types (so 'this' and 'that' can be of different types), meaning you'll usually see Either a b
or something like Either String Int
.
Where Maybe
has a very easy analog in the concept of a null
value, Either
doesn't enjoy the same luxury. However, it is conceptually equivalent to the Result
type in Rust and F#, even if the order of the arguments are flipped.
The data declaration (simplified) looks like this:
data Either a b = Left a | Right b
Either
has two data constructors, Left
and Right
, each of which take a single value. By convention, the Left
value is used for when something goes wrong, and the Right
value for when something goes, well, right.
Basic usage
Either
is usually used for error handling and validation, so it's ideal as a return type of functions that can 'fail' or that are otherwise not able to create a valid result based on the arguments given.
Let's return to our safeDiv
function from before. This time, however, we'll return an error message instead of Nothing
:
safeDiv :: Integral a => a -> a -> Either String a
safeDiv _ 0 = Left "You cannot divide by zero."
safeDiv x y = Right $ x `div` y
The error may be obvious in this case, but for more complex functions (or chains of them), it gets ever more useful.
For a slightly more advanced example, imagine you're creating a Person
record. In this case, a Person
has a name of type String
and an Age
of type Integer
. But with these constraints, we can easily pass in invalid values, so we'll have to validate that the name isn't an empty string and that the age is non-negative:
data PersonError = InvalidName | InvalidAge
data Person = Person
{ name :: String
, age :: Integer
}
makePerson :: String -> Integer -> Either PersonError Person
makePerson name age = undefined -- exercise for the reader ;)
The actual implementation of the validation isn't important in this case, so I've left that as an exercise for the reader. Oh, and what if there are multiple errors? Well, we can deal with that too, but that's not for now.
Further reading
If you have found the article useful and would like to learn more about Maybe
, Either
, and error handling, here's a little list of useful resources:
- The Haskell Wiki
- The Haskell wiki is usually one of the first places I
check when I want more information on anything Haskell. They have an article on Maybe as well as one on error handling, which has sections on both ~Maybe~ and ~Either~.
Learn You a Haskell is a pretty good introduction to Haskell that's available in its entirety from the website. The content in the Fistful of Monads chapter is a bit more advanced and might require reading some of the previous chapters, but both the chapter and the book could be well worth a look.
- The School of Haskell and the chapter on error handling ::
While I've not used The School of Haskell as a resource myself, I did find the error handling chapter to be well written. Oh, and it's written by Bartosz Milewski so you can expect some interesting insights.