Visit the bob exercise on Exercism to read the full instructions and download the exercise files.
Dig Deeper
Using String
Using String
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][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.
[hoogle]:
https://hoogle.haskell.org/
"Hoogle"
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'
Using Text
Using Text
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.
Using dependencies
The Text type and associated functions live in the Data.Text module of the external text package.
To be able to use it, you need to add this package to the list of dependencies in package.yaml:
dependencies:
- base
- text # 👈 Add this line
Thereafter you can import functions as you would normally:
-- allow using the following names by themselves
import Data.Text (Text, strip, unsnoc)
-- require all other names from `Data.Text` to be prefixed with `Text.`
import qualified Data.Text as Text
Language extensions
For various reasons, some of GHC’s features are locked behind switches known as language extensions.
You can enable these by putting so-called language pragmas at the top of your file:
-- These 👇 are language pragmas
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
module Bob (responseFor) where
{-
The rest of your code here
-}
OverloadedStrings
By default, "abc" is interpreted by the compiler as denoting a String.
To get a Text value instead, you need to explicitly convert using Text.pack:
someString :: _ -- compiler infers `String`
someString = "abc"
someText :: _ -- compiler infers `Text`
someText = Text.pack "abc"
This is a bit inconvenient.
The OverloadedStrings extension allows you to use string literals for Text as well.
-- This only works with OverloadedStrings enabled
someText :: Text
someText = "abc"
ViewPatterns
Recall, patterns occur in the following positions:
-- in `case` expressions
_ = case expression of
pattern -> expression
-- in function definition syntactic sugar
name pattern pattern = expression
The most common kinds of patterns are
_ = case e of
x -> _ -- binding pattern
Nothing -> _ -- constructor pattern
_ -> _ -- wildcard pattern
The ViewPattern extension adds the view pattern to the language, which has the form expression -> pattern.
When a value is matched against it, first the expression is applied to it as a function, and then the result of this is matched against the pattern.
One of its uses is the ‘pre-processing’ of arguments before pattern matching on them.
minimum :: Ord a => [a] -> Maybe a
minimum (sort -> xs) =
case xs of
[] -> Nothing
x : _ -> Just x
This implementation of minimum first sorts its argument, before pattern matching on it to retrieve the first element (if present).
Another way of writing this function, without the view pattern, is
minimum :: Ord a => [a] -> Maybe a
minimum unsorted =
let xs = sort unsorted
in case xs of
[] -> Nothing
x : _ -> Just x
This is a bit more verbose.
But more importantly it introduces an extra name: unsorted.
We intend to use it only once (to sort it), but we might accidentally use it in more places.
By eliminating the need for an extra name, the view pattern makes it impossible to make this mistake.
The solution highlighted above uses a view pattern to remove initial and trailing spaces from the input.
That way, isSilent and isQuestion need not do it themselves anymore, or the introduction of an otherwise unnecessary name (for the stripped query) is avoided.
Source: Exercism haskell/bob