This problem requires checking against a series of conditions, each associated with a desired output. This suggests using guards.
This problem might tempt you into reaching for last
.
However, you should know that it is dangerous:
ghci> tail []
*** Exception: Prelude.tail: empty list
A crash! That's no good.
Functions that do not return when given certain arguments (e.g. because they crash or get stuck in a loop) are known as partial functions. These functions display behavior (e.g. crashing) that is not documented in their types, which makes reasoning about code that uses them more difficult. For this reason, strive to avoid partial functions. It is almost always fairly easy to do so.
At multiple points in your solution you'll need to know whether the query is a question, or yelled. Do not copy–paste the code for determining this. Instead, give meaningful names to these computations and then use these names to determine whether the query is a yelled question.
Likewise, if you strip the query, do so only once.
Giving meaningful names to subexpressions can do wonders for code readability.
where
clauses allow you to list local definitions at the end of the declaration.
This allows you to paint the broad strokes of your strategy first, and to fill in the details later.
Defined values are computed lazily, so you do not need to worry about accidentally performing expensive but unnecessary computations.
More on where
elsewhere:
- Haskell Wikibook:
where
clauses - Learn You A Haskell: Syntax in Functions – Where!?
- Haskell Wiki: Let vs. Where
Many beginning Haskellers write code like
responseFor query
| isSilent query = "Fine. Be that way!"
| isQuestion query && isYelled query = "Calm down, I know what I'm doing!"
| isQuestion query = "Sure."
| isYelled query = "Whoa, chill out!"
| otherwise = "Whatever."
where
isSilent xs = _
isQuestion xs = _
isYelled xs = _
All three of the functions isSilent
, isQuestion
, and isYelled
here are only ever given the same argument: query
.
Therefore, we could replace their parameters with query
in their bodies and it wouldn't make a difference.
That is, supposing for example
isSilent :: String -> Bool
isSilent xs = all isSpace xs
we could change it into
isSilent :: String -> Bool
isSilent xs = all isSpace query
and the result would be exactly the same, as xs
would always be query
anyway.
But then isSilent
would become a function of one argument that it doesn't actually use.
We might as well remove the unused parameter and thereby turn the function into a constant.
isSilent :: Bool
isSilent = all isSpace query
This has a nice effect on the readability of the surrounding code:
responseFor query
| isQuestion && isYelled = "Calm down, I know what I'm doing!"
-- vs.
| isQuestion query && isYelled query = "Calm down, I know what I'm doing!"
where
isQuestion = _
isYelled = _
responseFor :: String -> String
responseFor query
| isSilent = "Fine. Be that way!"
| isQuestion && isYelled = "Calm down, I know what I'm doing!"
| isQuestion = "Sure."
| isYelled = "Whoa, chill out!"
| otherwise = "Whatever."
where
isSilent = all isSpace query
isQuestion = lastMay (filter (not . isSpace) query) == Just '?'
isYelled = any isLetter query && not (any isLower query)
This solution uses any
and all
to determine whether the query consists entirely of whitespace, and whether all letters are uppercase.
It also eschews last
, which is partial, in favor of the safe alternative lastMay
.
Read more about this approach.
responseFor :: Text -> Text
responseFor (strip -> query)
| isSilent = "Fine. Be that way!"
| isQuestion && isYelled = "Calm down, I know what I'm doing!"
| isQuestion = "Sure."
| isYelled = "Whoa, chill out!"
| otherwise = "Whatever."
where
isSilent = Text.null query
isQuestion = (snd <$> unsnoc query) == Just '?'
isYelled = Text.any isLetter query && not (Text.any isLower query)
String
is a very simple but inefficient representation of textual data.
This solution works with Text
instead, which is a data type designed specifically for working with text.
It also employs a view pattern.