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.
Using dependencies
The function lastMay lives in the Safe module of the external safe package.
To be able to use it, you need to add this package to the list of dependencies in package.yaml:
dependencies:
- base
- safe # 👈 Add this line
Thereafter you can import functions as you would normally:
import Safe (lastMay)
any & all
any and all are higher-order functions that take a predicate (a function that produces a Boolean) and a list as arguments.
Both check whether elements of the list satisfy the predicate.
any produces True when there is at least one element that satisfies the predicate, and False otherwise.
In contrast, all produces True only when all elements satisfy the predicate.
-- >>> any even [1, 3, 5] -- no even numbers in this list
-- False
-- >>> any even [1 .. 5] -- at least one even number in this list
-- True
-- >>> all even [2, 4, 6] -- all numbers in this list are even
-- True
-- >>> all even [2 .. 6] -- not all numbers in this list are even
-- False
How do these work?
To find they actual definitions of any and all, look up their documentation (for example through Hoogle) and click on the «Source» link next to the type signature.
The definitions of any and all look kinda complicated!
They are this way so that they can also be used on types other than lists, such as Set, Map, and Tree.
Specifically, they can be used on any type that is an instance of the Foldable type class.
In the source code, you can click on names to jump to their definitions.
This doesn't really work for foldMap here though: it sends you to its default (general) implementation, but here we need its implementation for lists specifically.
To find that code, navigate to the documentation of Foldable, look up [] in the list of «Instances», and click «Source».
It might be hard to see – even after lots of clicking through to definitions – but these definitions of any and all work essentially the same way as the one outlined here below.
Here is a possible definition of any:
or :: [Bool] -> Bool
or = foldr (||) True
any p = or . map p
And this is how it evaluates:
_ = any (2 <) [1..]
== (or . map (2 <)) [1..]
== or ( map (2 <) [1..] )
== foldr (||) True ( map (2 <) [1..] )
-- look at next element of the list
== foldr (||) True ( 2 < 1 : map (2 <) [2..] )
== 2 < 1 || foldr (||) True ( map (2 <) [2..] )
== False || foldr (||) True ( map (2 <) [2..] )
== foldr (||) True ( map (2 <) [2..] )
-- look at next element of the list
== foldr (||) True ( 2 < 2 : map (2 <) [3..] )
== 2 < 2 || foldr (||) True ( map (2 <) [3..] )
== False || foldr (||) True ( map (2 <) [3..] )
== foldr (||) True ( map (2 <) [3..] )
-- look at next element of the list
== foldr (||) True ( 2 < 3 : map (2 <) [4..] )
== 2 < 3 || foldr (||) True ( map (2 <) [4..] )
== True || foldr (||) True ( map (2 <) [4..] )
-- (||) short-circuits
== True
As you can see, evaluation terminates as soon as a number larger than 2 is found.
And thanks to laziness, any and all even work on infinite lists!
Provided the answer can be determined after looking at finitely many elements, that is.
In this approach
A query is considered silent when it consists entirely of whitespace characters. Which is to say: it is silent when all of its characters are whitespace. Hence,
isSilent = all isSpace query
Similarly, a query is considered yelled when all its letters are uppercase, provided there is at least one letter.
This latter condition is expressed with any:
atLeastOneLetterPresent = any isLetter query
That all letters should be uppercase is trickier to express.
all isUpper query would not do, as non-letters do not count as uppercase.
-- >>> isUpper ' '
-- False
-- >>> isUpper '!'
-- False
-- >>> all isUpper "HI THERE!"
-- False
One way of working around this is filtering out non-letters first.
-- >>> filter isLetter "HI THERE!"
-- "HITHERE"
-- >>> all isUpper (filter isLetter "HI THERE!")
-- True
Another is defining a combinator that combines predicates.
implies :: (a -> Bool) -> (a -> Bool) -> a -> Bool
p `implies` q = \x -> if p x then q x else True
-- >>> all (isLetter `implies` isUpper) "HI THERE!"
-- True
And yet another way is observing that if all letters are uppercase then there are no lowercase letters, and vice versa.
-- >>> all (not . isLower) "HI THERE!"
-- True
-- >>> not (any isLower "HI THERE!")
-- True
all (not . p) xs and not (any p xs) are entirely equivalent, for all p and xs.
A query is considered a question when its last non-whitespace character is '?'.
To get at this character, the solution highlighted above first filters out all whitespace.
After this, it could have used last, but that function crashes when given an empty list:
ghci> tail []
*** Exception: Prelude.tail: empty list
To avoid such crashes, the alternative lastMay function is used.
Instead of crashing, it will return Nothing.
And when the list is not empty lastMay returns a Just.
-- >>> lastMay ""
-- Nothing
-- >>> lastMay "abc"
-- Just 'c'