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'