Building Complete Interactive Programs

Introduction: Assembling the Capstone

Welcome to the seventh and final lesson of this course! You have made it to the capstone of our Haskell journey. Over the previous lessons, you learned how to model information using custom types, how to handle missing data and errors safely with Maybe and Either, and how to read user input using the IO system.

Now, we are going to combine all of those pieces to build a complete, interactive command-line task manager.

Our goal is to create a program that continuously accepts user commands so we can find, list, and add tasks. To keep the command parsing beginner-friendly, this final version will accept single-word titles for the add command, such as add laundry. A command like add write report will not be recognized by this simple parser because it splits input with words and matches exactly two words. To do this, we will use a common and powerful pattern in functional programming: we will separate our program into a pure core that handles data logic and a thin impure IO shell that interacts with the user. Let us start by building the pure core.

Handling Commands: The Pure Core

The brain of our program will be a function called handle. This function does not print anything to the screen or read from the keyboard. It simply takes the current list of tasks and a list of command words from the user and decides what to do.

Let us start by defining its signature and a fallback for commands it does not recognize.

Haskell
handle :: [Task] -> [String] -> (String, [Task])
handle tasks _ = ("Commands: list | add <title> | find <id>", tasks)

Notice the return type: (String, [Task]). When we give handle a command, it returns a tuple containing two things:

  1. A message to show the user (String).
  2. The list of tasks, which might be updated ([Task]).

Because the catch-all pattern (_) simply provides help text, it returns the tasks exactly as it received them.

Next, let us add a command to find a task by its ID. You might remember our parseFind function from a previous lesson, which safely searches for a task and returns a formatted string. We can reuse it here!

Haskell
handle :: [Task] -> [String] -> (String, [Task])
handle tasks ["find", arg] = (parseFind tasks (readMaybe arg), tasks)
handle tasks _             = ("Commands: list | add <title> | find <id>", tasks)

When the user types something like find 1, the words are split into the list ["find", "1"]. Our new clause catches this pattern. It uses readMaybe to safely turn the "1" into a number, passes it to parseFind to get the output message, and returns the list of tasks unchanged.

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