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:
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.
And, as we hinted earlier, separating “tacocat” into two words also throws it off.
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
- 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
nonemptyPal have the same type, all we have to do is replace
nonemptyPal in the definition of
Load this into the REPL and test it. We can see that it does work as expected.
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
True, the cases are nothing and something. We call this type
Maybe has two cases:
Nothing, which often indicates that no valid input was received;
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
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
Bool) has two cases, the empty list and the nonempty list. What we care about right now is matching the empty list to a
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
main still work?
We need to know if
Maybe Bool works with the
Show class. We can find this information in GHCi again, by querying for info about
λ> :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
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.
Try to write a similar function, using this
case syntax. Call the function
verbose and give it the type
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.”