- 15 minutes
Now that we have written a palindrome-testing function, what we want to do next is turn this into a program instead of just a function.
About I/O
A Haskell program needs to have an “entry point” called main
. Normally, when we write Haskell, the order that we add stuff to a file doesn’t matter. We can define functions and types in any order that we think of them. But when we call main
to run a program – within the definition of main
, the ordering does matter, because calling main
sets off a sequence of events. So everything that we write after the main
name has to be in the order we want it to happen when we run the program. To help with this, we can use some special syntax (the do
keyword).
We’re going to see that now, but we won’t really be examining it yet or getting into the mechanics of it until later. When we do get to those explanations, you’ll see that it’s not really special to main
, exactly, it’s special to something called monads and, in this case, the IO
type that appears in the type of every main
action. The type declaration usually looks like this:
IO
stands for input/output, and it’s something all programs have to be able to do: take inputs, print outputs to a screen, sending messages over a network, and so forth. Doing things like this is what makes code a program, rather than just a collection of functions. In Haskell, IO
is a type which is quite different from what you might expect in other programming languages in which I/O is implicit and pervasive.
isPalindrome
, having the type String -> Bool
, doesn’t do any sort of I/O. When we use it the REPL, GHCi does some I/O for us (it prints the result), but isPalindrome
itself, having the type it has, can’t prompt a user for input, print to the screen by itself, etc. Those are IO
things.
Reading input
OK, enough about that. Our main
action here will be very small for the time being. We only have one function, after all! But we want to take “user” input – the users will only be ourselves for now, but that’s plenty of users for our first program.
We’re going to use another Prelude
function in order to do this. Let’s look at it for a moment. It’s called getLine
and we’ll ask to see its type first.
λ> :type getLine
getLine :: IO String
So here we see that IO
type that we mentioned – getLine
is an IO String
. The String
is what we want to work with for our isPalindrome
function, but since we want the String
input to come from user input this is what we need – the ‘I’ of ‘IO’ stands for ‘input’. Let’s see what happens in GHCi when we use getLine
.API documentation for getLine
Type getLine
at the prompt and hit return.
λ> getLine
The cursor moves to the next line down, and it’s waiting for you to type something. Type in your name. You don’t need to add quotation marks or anything. Hit return.
λ> getLine
Julie
"Julie"
GHCi runs an implicit “print” command that prints the input to the screen. You provided some input, and now GHCi prints some output. It will add quotation marks to your name because the result of that printing is a String
, and strings in Haskell have quotation marks around them.
Using the input
OK, so the first thing we know we need to do in our main
action is use this getLine
to get some user input, and we know we can get a String
from that. We can’t directly get a String
input from it, though. An IO String
is not the same thing as a String
! GHCi will tell us that if we try to apply isPalindrome
to getLine
.
λ> isPalindrome getLine
<interactive>:20:14: error:
• Couldn't match type ‘IO String’ with ‘[Char]’
Expected type: String
Actual type: IO String
The expected type is the type that isPalindrome
needs as input; the actual type is, well, the actual type of the value we tried to pass to it. The type isPalindrome
expects as input is String
but getLine
delivers an IO String
. Those are not the same type. So we need to figure out how to make those work together.
The first part of the solution to this problem is going to be found in that “special syntax” for main
we mentioned earlier. So let’s start on that. do
is the keyword (not main
) that allows us to use this special syntax, so we need to make sure to get that in there. The type of main
in Haskell is almost always IO ()
The empty parentheses represents a value called “unit”, so we pronounce IO ()
as “IO unit”. which tells us that main
does input/output and its only “return value” is output to a screen or some other kind of effect (in this program, the effect is printing the result of the function evaluation to the screen).
The first thing we’ll do within the do
block is pick a variable name – we’ve chosen to name it word
– then write this leftwards arrow <-
, which is part of the special do
syntax, and getLine
.
This left arrow binds the String
from getLine
to this variable name, word
. Now the IO
hasn’t disappeared, but using this arrow to bind just the String
to a variable name allows us to pass that String
as an input to a function like isPalindrome
.
main :: IO ()
=
main do
<- getLine
word isPalindrome word
But this doesn’t quite work yet, because we haven’t sufficiently dealt with the IO
that, as we said, didn’t really go away.
Printing output
As we said, GHCi does the printing for you when you evaluate stuff at the prompt by implicitly running a print
command.
λ> isPalindrome "tacocat"
True
When we apply a function in the REPL, the isPalindrome
function itself isn’t what’s printing the output. GHCi is doing that implicitly, and that’s why we see True
on the screen. But our main
action needs to represent an entire program that can do everything it needs to do on its own without GHCi, so we have to handle that print
command explicitly. So, let’s look at that next.
Query the type of print
.API documentation for print
λ> :type print
print :: Show a => a -> IO ()
We can read this, we’ve seen this kind of type before. print
is a function that can take as input one value of any type that is a member of the Show typeclass, and it will return this IO ()
– it will return an effect. As we said about the type of main
the practical meaning of IO ()
for us is that it returns an output to the screen.
Let’s stop and think for a moment. print
can print anything to the screen as long as it is a member of the Show
class. We can tell you that most built-in types in Haskell are members of the Show class (with one very important exception that we’ll discuss later), but what we want to teach you is how to find out for yourself if the print
function fits our need.
Querying instances
We saw in an earlier session that there’s an :info
command that can give us information. That seems helpful, but we need to think about what kind of information we need. Let’s be clear about the pieces to this puzzle we’re trying to put together:
- The output of
isPalindrome
is aBool
value. - The final type of
main
isIO ()
(output to the screen). print
can take some kinds of inputs and return printing to the screen, so it might work as the way to glue the output ofisPalindrome
into theIO
structure we need.
We can only know if it will work if we know whether Bool
is a member of the Show
class that print
needs as input. So, that’s what we need to ask. We can query it as :info Bool
or :info Show
– either one will get us the information we want.
λ> :info Bool
data Bool = False | True -- Defined in ‘GHC.Types’
instance Eq Bool -- Defined in ‘GHC.Classes’
instance Ord Bool -- Defined in ‘GHC.Classes’
instance Show Bool -- Defined in ‘GHC.Show’
λ> :info Show
class Show a where
showsPrec :: Int -> a -> ShowS
show :: a -> String
showList :: [a] -> ShowS
{-# MINIMAL showsPrec | show #-}
-- Defined in ‘GHC.Show’
instance (Show a, Show b) => Show (Either a b)
-- Defined in ‘Data.Either’
instance Show a => Show [a] -- Defined in ‘GHC.Show’
instance Show Word -- Defined in ‘GHC.Show’
instance Show GHC.Types.RuntimeRep -- Defined in ‘GHC.Show’
instance Show Ordering -- Defined in ‘GHC.Show’
instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
instance Show Integer -- Defined in ‘GHC.Show’
instance Show Int -- Defined in ‘GHC.Show’
instance Show Char -- Defined in ‘GHC.Show’
instance Show Bool -- Defined in ‘GHC.Show’
What we’re looking for is this line, which appears in the output of both :info
queries above:
instance Show Bool
This means “Bool
has an instance of Show
” – In other words, Bool
is a member of the Show
class. So thanks to print
’s polymorphic type, it will work for us!
Finishing up
Revise main
by applying print
to the result of isPalindrome word
– that is the result we want printed to the screen. We’ll put parentheses around that expression and print
on the outside.
Load the file into GHCi (or reload, if you already have it loaded) and try it out! Run main
, enter some input which may or may not be a palindrome, and press enter.
λ> :reload
[1 of 1] Compiling Main ( palindrome.hs, interpreted )
Ok, one module loaded.
λ> main
julie
False
it :: ()
λ> main
tacocat
True
Make sure you have what we’ve done so far typed into a file; we’ll be expanding upon it in the next sessions.