The Show
typeclass is for debugging
The Show
typeclass is really meant to be derived, not implemented yourself. The purpose of Show
is mostly for debugging, and the closest thing to a law for Show
is that it should be the inverse of Read
. The result of show
should usually be a string that represents a Haskell expression that would produce the value itself.
While it is tempting to create a custom instance of Show
for your own purposes, I would just convert your instance into a separate function, showOperator
(which can also be implemented more simply with direct pattern matching rather that explicitly using case
):
showOperator :: Operator -> String
showOperator Add = "+"
showOperator Subtract = "-"
showOperator Multiply = "-"
randomSensibleInts
is poorly-named
What makes integers in the range [1, 10] any more “sensible” than any other integers? Given how incredibly simple it is, and given it’s only used in one place, I would honestly just inline it into the place it’s used. I think it’s clearer that way than any name you could give it.
Avoid !!
The !!
function is unsafe, since it will crash if the provided index is out of bounds. Admittedly, in your case, you know the length of the list will always be exactly 2, so it’s not dangerous, but in that case, you should just use pattern-matching instead:
genQuestion :: StdGen -> (Int, Operator, Int, StdGen)
genQuestion gen = (x, operator, y, gen')
where [x, y] = take 2 $ randomRs (1, 10) gen
(operator, gen') = random gen
(You also don’t need the type annotation on random gen
, so I’ve removed it.)
Package tuples into named datatypes
The long tuple return type for genQuestion
is a little bit suspicious. The name even provides a good suggestion for naming the datatype: Question
. Create a datatype for that instead of having such a large tuple:
data Question = Question Int Operator Int
genQuestion :: StdGen -> (Question, StdGen)
genQuestion gen = (Question x operator y, gen')
where ...
askQuestion :: (Question, StdGen) -> IO Bool
askQuestion (Question i1 o i2, gen) = ...
Avoid using booleans directly
Using Bool
as a return type of a non-predicate function can be confusing (this is referred to as “boolean blindness”). Using a separate, named type can be both more readable and more scalable if the number of cases ever changes:
data AnswerResult = Correct | Incorrect
deriving (Eq)
askQuestion :: (Question, StdGen) -> IO AnswerResult
askQuestion = ...
This also allows adjusting the score summing logic to be a bit more reliable. Using toEnum
and assigning significance to the indices is both unnecessary and mixing concerns.
putStrLn $ "Your score was " ++ show (length $ filter (== Correct) answers)
Avoid mixing pure and impure logic
The askQuestion
function has too many responsibilities: formats a prompt for input, displays it to the user, calculates the answer to the question, validates the answer to the question, and prints out a response based on the answer. Three of those responsibilities are totally pure, and they should ideally be pulled into their own functions:
questionPrompt :: Question -> String
questionPrompt (Question x op y) = "Enter the answer to " ++ show x ++ showOperator op ++ show y ++ ": "
questionAnswer :: Question -> Int
questionAnswer (Question x op y) = operatorFunction op x y
where operatorFunction Add = (+)
operatorFunction Subtract = (-)
operatorFunction Multiply = (*)
validateAnswer :: Question -> Int -> AnswerResult
validateAnswer question answer =
if questionAnswer question == answer
then Correct
else Incorrect
askQuestion :: (Question, StdGen) -> IO AnswerResult
askQuestion (question, gen) = do
putStrLn $ questionPrompt question
userAnswer <- getLine
let answerResult = validateAnswer question (read userAnswer)
case answerResult of
Correct -> putStrLn "Well done!"
Incorrect -> putStrLn $ "Wrong - the answer was " ++ show (questionAnswer question)
return answerResult
Eliminate unused variables
If you turn on -Wall
, GHC will tell you that gen
is never used in askQuestion
at all, so it can be removed:
genQuestion :: StdGen -> Question
genQuestion gen = Question x operator y
where [x, y] = take 2 $ randomRs (1, 10) gen
(operator, _) = random gen
askQuestion :: Question -> IO AnswerResult
askQuestion question = do
putStrLn $ questionPrompt question
userAnswer <- getLine
let answerResult = validateAnswer question (read userAnswer)
case answerResult of
Correct -> putStrLn "Well done!"
Incorrect -> putStrLn $ "Wrong - the answer was " ++ show (questionAnswer question)
return answerResult
main :: IO ()
main = do
gens <- mapM (const newStdGen) [1..10]
let questions = map genQuestion gens
answers <- mapM askQuestion questions
putStrLn $ "Your score was " ++ show (length $ filter (== Correct) answers)
Use where
or let
instead of case
when there is only one pattern
In the Random
instance for Operator
, it’s possible to avoid case
if you’re simply destructuring rather than matching against multiple patterns:
instance Random Operator where
random gen = (toEnum item, gen')
where (item, gen') = randomR (0, 2) gen
randomR (lo, hi) gen = (toEnum item, gen')
where (item, gen') = randomR (fromEnum lo, fromEnum hi) gen
Final result
import System.Random
data Operator = Add | Subtract | Multiply
deriving (Eq, Enum)
data Question = Question Int Operator Int
data AnswerResult = Correct | Incorrect
deriving (Eq)
instance Random Operator where
random gen = (toEnum item, gen')
where (item, gen') = randomR (0, 2) gen
randomR (lo, hi) gen = (toEnum item, gen')
where (item, gen') = randomR (fromEnum lo, fromEnum hi) gen
showOperator :: Operator -> String
showOperator Add = "+"
showOperator Subtract = "-"
showOperator Multiply = "-"
questionPrompt :: Question -> String
questionPrompt (Question x op y) = "Enter the answer to " ++ show x ++ showOperator op ++ show y ++ ": "
questionAnswer :: Question -> Int
questionAnswer (Question x op y) = operatorFunction op x y
where operatorFunction Add = (+)
operatorFunction Subtract = (-)
operatorFunction Multiply = (*)
validateAnswer :: Question -> Int -> AnswerResult
validateAnswer question answer =
if questionAnswer question == answer
then Correct
else Incorrect
genQuestion :: StdGen -> Question
genQuestion gen = Question x operator y
where [x, y] = take 2 $ randomRs (1, 10) gen
(operator, _) = random gen
askQuestion :: Question -> IO AnswerResult
askQuestion question = do
putStrLn $ questionPrompt question
userAnswer <- getLine
let answerResult = validateAnswer question (read userAnswer)
case answerResult of
Correct -> putStrLn "Well done!"
Incorrect -> putStrLn $ "Wrong - the answer was " ++ show (questionAnswer question)
return answerResult
main :: IO ()
main = do
gens <- mapM (const newStdGen) [1..10]
let questions = map genQuestion gens
answers <- mapM askQuestion questions
putStrLn $ "Your score was " ++ show (length $ filter (== Correct) answers)