Modeling Absence with Maybe

The "Not Found" Problem and the Maybe Type

Welcome to Unit 4! In our previous lessons, you learned how to create descriptive type synonyms, define custom data types like our Task model, and use pattern matching to look inside those custom types. Now, we are going to tackle a very common real-world problem: searching for an item that might not exist.

Imagine looking up a task by its ID. What happens if you provide an ID for a task that is not in your list? In many programming languages, trying to access a missing item causes the program to crash or silently return an unsafe empty value (like null), leading to errors later on.

Haskell handles this safely by making "not found" an ordinary, expected value. It does this using a built-in type called Maybe. You can think of Maybe as a physical box.

  • If the search is successful, the box contains exactly one item. We represent this using the word Just, followed by the item (for example, Just task).
  • If the search fails, the box is completely empty. We represent this using the word Nothing.

By using Maybe, we make absence explicit in the type of the function. That encourages us to handle both the success and failure cases. When we pattern match on both Just and Nothing, as we will in this lesson, our lookup can handle missing tasks without crashing.

Setting up a Helper Function

Before we write our safe search function, let's prepare a small helper function. As a reminder, here is the custom Task data type we built in previous lessons:

Haskell
type TaskId = Int
type Title = String
data Status = Todo | Done deriving (Show, Eq)

data Task = Task TaskId Title Status deriving (Show, Eq)

To search through a list of tasks, we will frequently need to check the ID of a given task. Instead of writing out the full pattern match every time, we can create a small helper function called taskId.

Haskell
taskId :: Task -> TaskId
taskId (Task tid _ _) = tid

Because we only care about the ID, we use underscores _ to ignore the title and the status. This function takes a Task, looks inside it using pattern matching, and returns just the TaskId.

Writing a Safe Search Function

Now we are ready to write our findTask function. We want to provide a TaskId and a list of Task items, and we want it to return a Maybe Task (our safe box).

Let's start by defining the signature and the most basic scenario: an empty list.

Haskell
findTask :: TaskId -> [Task] -> Maybe Task
findTask _ [] = Nothing

If the list is empty [], it means we have run out of tasks to check. Therefore, the task we are looking for is definitely not there. We return Nothing. Notice that we use an underscore _ for the ID we are searching for, because if the list is empty, the ID we want does not matter.

Next, we handle the case where the list is not empty. We separate the list into the first task t and the rest of the list.

Haskell
findTask :: TaskId -> [Task] -> Maybe Task
findTask _ [] = Nothing
findTask wanted (t : rest)
  | taskId t == wanted = Just t
  | otherwise          = findTask wanted rest

Here, we use guards (the | symbol) to check a condition:

  • If the taskId of our current task t equals the wanted ID, we found a match! We safely place the task in our box by returning Just t.
  • otherwise, if they do not match, we call findTask again to keep searching through the rest of the list.
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal