When I first started writing my number guessing game in Haskell, I had a clear goal: let users pick a difficulty, guess a number, and count how many tries it took them to win. Sounds simple enough Well, it didn’t go as smoothly as I thought I’ll walk you through what went wrong, how I fixed it, and what I learned.
What I Want to Build
The concept was straightforward:
- Present a menu to the user with three difficulty levels.
- Generate a random number based on the difficulty.
- Let the user guess repeatedly until they get it right.
- Count how many guesses it takes and display that at the end.
The Syntax Error That Stopped Everything
Here’s the first version of my code that tripped me up:
choice <- readLn
| choice == 1 = "You selected easy" >> easy
| choice == 2 = "You selected medium" >> medium
| choice == 3 = "You selected hard" >> hard
| otherwise = "You did not selected a valid choice" >> main
At first glance, it seemed logical. I was trying to pattern match on the value of choice
using guards (|
). But Haskell didn’t like it at all.
The Error
Parse error in pattern: putStrLn
Possibly caused by a missing 'do'?
That message pointed me in the wrong direction at first. But the real issue was this:
You cannot use guards (|
) outside of a function definition. They’re only valid inside things like case
, if
, or when defining a function with multiple conditions.
Use case
Expression
So I rewrote the logic using a case
statement, which is perfectly suited for handling multiple choices. That solved the error and made the code much cleaner.
Adding a Guess Counter
Once the syntax was fixed, I wanted to add a new feature: count how many guesses the user makes.
The approach was to pass a counter variable (count
) into the game loop and increment it with each incorrect guess. This way, when the user guesses correctly, we can display how many tries it took.
Final Working Code
Here’s the final version of the game with everything put together:
import System.Random (randomRIO)
main :: IO ()
main = do
putStrLn "Do you want to play:"
putStrLn "1) Easy\n2) Medium\n3) Hard"
putStrLn "Select your difficulty by entering a number (1, 2 or 3):"
choice <- readLn :: IO Int
case choice of
1 -> putStrLn "You selected easy" >> easy
2 -> putStrLn "You selected medium" >> medium
3 -> putStrLn "You selected hard" >> hard
_ -> putStrLn "Invalid choice, try again." >> main
easy :: IO ()
easy = do
putStrLn "Guess a number between 1 and 13:"
rNumber <- randomRIO (1, 13)
loop rNumber 1
medium :: IO ()
medium = do
putStrLn "Guess a number between 1 and 25:"
rNumber <- randomRIO (1, 25)
loop rNumber 1
hard :: IO ()
hard = do
putStrLn "Guess a number between 1 and 50:"
rNumber <- randomRIO (1, 50)
loop rNumber 1
loop :: Int -> Int -> IO ()
loop rNumber count = do
putStr "Your guess: "
userInput <- readLn
case compare userInput rNumber of
EQ -> putStrLn (" Correct! It took you " ++ show count ++ " guesses.") >> main
LT -> putStrLn "Too low! Try again." >> loop rNumber (count + 1)
GT -> putStrLn "Too high! Try again." >> loop rNumber (count + 1)
What I Learn
- Guards only work in function definitions, not inside
do
blocks. case
expressions are perfect for branching based on user input.- Haskell’s type system and error messages may seem cryptic at first, but they guide you toward good patterns.
- Adding state (like a guess counter) is easy in Haskell by passing parameters recursively.
Final Thought
This project reminded me that even simple games can teach you powerful lessons. Whether you’re new to Haskell or just exploring functional programming, small projects like this are the best way to get comfortable with pattern matching, recursion, and IO.