Thanks for sticking with us, and congratulations on having your first working Haskell program! From here, we’ll be working on improvements to our palindrome program.
Room for improvement
There are ways in which our palindrome test is not quite satisfactory, even though we have our basic functionality working. For example, at the moment it considers empty strings to be palindromes, even though that’s probably not exactly what we want. Run main
and just hit enter without typing any input:
λ> main
True
It also won’t count a word as a palindrome if it has any capital letters that don’t match up, which might run contrary to our usual expectations. We know that “mom” is a palindrome, but it is not recognized if we capitalize it. Computers don’t know that M and m are the same thing – and in some sense, maybe they are right, but we would prefer to consider "Mom"
as a palindrome.
λ> main
Mom
False
And, as we hinted earlier, separating “tacocat” into two words also throws it off.
λ> main
taco cat
False
So we want to invent some ways to handle these things. We’ll deal with each of these potential improvements separately, and then eventually figure out ways to bring them all together.
Catching the empty string
Let’s start by thinking through what we want to do about empty strings. We will acknowledge at the outset that it would be weird of you, personally, as the only user of your program to enter an empty string! However, you’re doing an I/O here – anything could go wrong and you must be prepared for all contingencies!
We could fall back to our trusty conditional statement. If the input is equal to an empty string, for example, then we could return False, otherwise we would apply the isPalindrome
function to the input. This means that this function will take a String
input and return a Bool
output:
- False if the input is empty;
- True or False depending on if the non-empty input a palindrome.
We could alter our main
to use this instead of using isPalindrome
directly, too. Since isPalindrome
and nonemptyPal
have the same type, all we have to do is replace isPalindrome
with nonemptyPal
in the definition of main
:
Load this into the REPL and test it. We can see that it does work as expected.
λ> main
False
λ> main
Julie
False
But we might find it a little unsatisfying that an empty string gives us the same result as a non-empty string that isn’t a palindrome. It seems less than ideal that the output does not distinguish between these two situations. There is still something we can improve upon.
Perhaps, perhaps, perhaps
Earlier we mentioned that there’s a type we sometimes use for cases like this where you want to discriminate a real result from a rejection of invalid input. Like Bool
, this type has two cases – but rather than False
and True
, the cases are nothing and something. We call this type Maybe
.
Remember, Maybe
has two cases:
Nothing
, which often indicates that no valid input was received;Just <something>
.
In our present circumstance, we could use Nothing
to indicate that there was an empty string input, and use the Just
constructor to return the Boolean result of applying isPalindrome
to the nonempty string. This gives us a way to discriminate the cases where we want to say “this isn’t valid input” and “this isn’t a palindrome,” which seems useful.
To do this, let’s go ahead and change the nonemptyPal
function. This time, we have to change the type signature: it is now returning not just a Bool
but a Maybe Bool
– the True
or False
result might not be present!
If you don’t like to delete things, you can write a new function instead of modifying nonemptyPal
, but keep in mind that your two functions will need to have different names. Or you may want to comment out the old function, by placing two hyphens before each line. This will allow you to keep the old code around for reference.
Now what we’ll do is that pattern-matching that we introduced earlier. This time, though, we’ll be matching on the cases of the list type. We recall that the list type (like Maybe
and Bool
) has two cases, the empty list and the nonempty list. What we care about right now is matching the empty list to a Nothing
result.
You could use the square list brackets there or the double quotation marks that are special for strings, it’s up to you; Haskell knows they both indicate lists so it doesn’t care which you use here.
So now what do we do for the other case? When we were matching on Bool
it was pretty clear what the two cases were, but when we were matching on Integers in our very first function, we used an underscore to catch all the other possible inputs. And that will work for us here, too.
In every case except the empty string that we’ve already covered, we want to apply the isPalindrome
to the word input, and then we want to apply the Just
constructor to that output. Fortunately, we can do that on the right side of a case arrow.
Remember the main
But we’ve changed the type of nonemptyPal
– it now outputs a Maybe Bool
instead of merely a Bool
. Does main
still work?
We need to know if Maybe Bool
works with the print
function, so it needs to have an instance of the Show
class. We can find this information in GHCi again, by querying for info about Maybe
.
λ> :info Maybe
data Maybe a = Nothing | Just a -- Defined in ‘GHC.Maybe’
instance Applicative Maybe -- Defined in ‘GHC.Base’
instance Eq a => Eq (Maybe a) -- Defined in ‘GHC.Maybe’
instance Functor Maybe -- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘GHC.Base’
instance Semigroup a => Monoid (Maybe a) -- Defined in ‘GHC.Base’
instance Ord a => Ord (Maybe a) -- Defined in ‘GHC.Maybe’
instance Semigroup a => Semigroup (Maybe a)
-- Defined in ‘GHC.Base’
instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
instance MonadFail Maybe -- Defined in ‘Control.Monad.Fail’
instance Read a => Read (Maybe a) -- Defined in ‘GHC.Read’
instance Foldable Maybe -- Defined in ‘Data.Foldable’
instance Traversable Maybe -- Defined in ‘Data.Traversable’
Finding an entry for Show
in that list gives us the answer to that question: Yes, there is a Show
instance for Maybe
.
instance Show a => Show (Maybe a)
So we don’t need to change anything in main
, thanks to the power of polymorphism.
We do need to save and reload into the REPL (or click run
in repl.it, perhaps). Try it on a series of inputs, at least one empty string, one palindrome, and one word that is not a palindrome to give yourself confidence that it works and you understand why it works.
Exercise
Try to write a similar function, using this case
syntax. Call the function verbose
and give it the type String
to String
.
It’s going to take the word input (ultimately, this word will come from getLine
, remember!), apply the nonemptyPal
to it, and match the three possible outputs of nonemptyPal
to strings that tell the user some information about the result. For example, “Yay, this is a palindrome!”, “Please enter a word.”, or “Sorry, this is not a palindrome.”