Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2024-05-24.
We’ve seen putStrLn
. It lets us output any String
on the screen.
What if we want to print out a number instead of a String
?
We have the following definition for an expression that is adding two numbers, and we want to print the resulting value out in a program.
number :: Integer
number = 100390 + 29389
(Integer
, by the way, is the type of unbounded whole numbers in Haskell. Unbounded means they have no upper or lower limit (or bounds). In contrast, the maxBound
of Int
that this document is being prepared on now is 9223372036854775807.)
So, we know that the putStrLn :: String -> IO ()
function takes a String
, and leaves us with an IO ()
value.
We don’t have a String
, though, we have an Integer
value. How can we match these up so we can print out our number on the screen?
In order to answer that, let’s first look at the type of the (+)
function. We know from experience that it takes a number and another number and returns their sum as a number, but the actual type isn't something we’ve seen, and includes some special new stuff for us. Let’s look:
(+) :: Num a => a -> a -> a
Ok, breathe. Let’s first look at the right side: a -> a -> a
. Why all the a
’s?
Well, we know from all the (->)
’s that this means it’s a function that takes two values of type “a
”, and returns a third “a
”. What is “a
”, though? It’s what’s called a type variable. That means it can be any type. If it starts with a lowercase letter, it’s a type variable. If it starts with a capital, it’s an actual type, or a typeclass, which we'll explain soon (Num is a typeclass in Num a => a
).
One important point about type variables is that while they can be any type at all, all the “a
”s must still be the same type as each other when using the function! So if we used an Integer
value as our first argument, so saying that the first type a
was Integer
in (+)
, then we would need to use an Integer
as our second argument, too:
-- both types must be the same,
-- so this will be fine:
goodNumber = (3 :: Integer) + (5 :: Integer)
-- this would cause a type error
willNotWork = (3 :: Int) + (5 :: Integer)
If we don't specify the type of our numbers, Haskell's type inference works it out for us, which saves a lot of time and hassle.
Ok, so here is another way to write the (+)
function, which shows you that variables don’t necessarily have to be named a
:
-- it's a lot smaller to write
-- 'a' than 'theNumber'
(+) :: Num theNumber =>
theNumber ->
theNumber ->
theNumber
Now we have to think about the “Num a =>
” part. That can be read as “a
is constrained to types which are instances of the Num
typeclass”. This mouthful means that the (+)
function can take two arguments of any type at all (here we’re naming them a
), as long as that type (again, here called a
) is an instance of a typeclass called Num
. Luckily for us, all numbers are!
A typeclass is not a concrete type like Integer
, Int
or String
. It’s a way of tagging many types (a “class” of them, if you like) so that we can have functions or values that work with many similar types that do similar things, but that are actually different. Let's take a look:
-- a small int 5:
intFive = 5 :: Int
-- a "floating-point" value of 10.3
floatTenPointThree = 10.3 :: Float
-- add them together with (+). This will not work...
-- because both types are concretized (or specialized)
errorResult = intFive + floatTenPointThree
-- add them together with (+). This will work
result = (fromIntegral intFive) + floatTenPointThree
-- the result is 15.3
Here we’re using fromIntegral
to build an “unspecialised” Num a => a
version of the Int
value of intFive
so we can subsequently add it to floatTenPointThree
. The value 5 :: Int
is not of type Float
, so the types won’t match unless we do this. However, if either of the values are of type Num a => a
, then (+)
will typecheck because it can match both types together (by concretizing the Num a => a
type to Float
).
The Float
, Int
, and Integer
types are all instances of the Num
typeclass. There are many numeric types in Haskell such as these. To be able to do arithmetic functions on different numeric typed values, they are tagged as Num
which allows us to define each of the simple arithmetic functions for each type differently, but use them all with the same name and interchange values.
This “tagging” is called making a type an instance of a typeclass. When a programmer does this, they provide a definition against the particular type, for the functions that the typeclass requires.
So we can see that (+)
can add a Float
or an Integer
to a Num a => a
without a problem. The Num
typeclass is, in this way, like a kind of contract that programmers of a type can decide to kind of "subscribe to" which gives them the ability to write implementations of the functions that the Num
typeclass provides. In turn, the typeclass system gives that type the ability to work with all the other types that are instances of that typeclass.
So the Num
typeclass means there actually isn’t only one definition for the functions for addition: (+)
, subtraction: (-)
, multiplication: (*)
, negation: negate
, etc but rather that each type — that is, each instance of Num
— has its own definition for each of these functions.
What does all of this have to do with printing our number on the screen? Remeber, that problem we started the chapter with?
Well, there’s a typeclass called Show
(with a big S), and this provides a single function: show
(with a small s), that can take any instance of Show
, and makes a String
version of it. Let’s look at the type of the show
function:
-- takes a "showable" thing
-- and returns a String
show :: Show a => a -> String
We see that show is a function which takes a single argument of any type (the “a
” type variable above) constrained to the Show
typeclass, and returns a String
. That single argument is anything that has an instance of Show
defined for it. We know this because of the Show a =>
constraint.
Printing things to the screen is such a common thing to do that many types have an instance of Show
already, including of course, Integer
and Int
, so getting back to the first program of this chapter, we can just apply show
to our Integer
and then pass it to putStrLn
. Let’s see how:
number :: Integer
number = 100390 + 29389
main :: IO ()
main = putStrLn (show number)
Parentheses are needed on show number
because putStrLn
only takes one argument, and the function application precedence rules mean that taking them off would give it two. Precedence is a fancy-pants word that simply means “which things come before or after which other things”. If we left off the parentheses, we would have this: putStrLn show number
, which Haskell would see as “apply putStrLn
to the value show
, and then apply that to the value number
”. However, putStrLn
takes only String
values, and show
is a function, so that would definitely be a type error.
We have one last trick up our sleeves to show you (pardon the pun). It’s the print
function. This is very similar to putStrLn :: String -> IO ()
, but rather than taking a String, it can take a value whose type is any instance of Show
! Let’s see a version of our program that uses print :: Show a => a -> IO ()
rather than putStrLn
:
number :: Integer
number = 100390 + 29389
main :: IO ()
main = print number
See if you can work out what the following program does.
number1 :: Num a => a
number1 = 1 + 5 + 7 + 3 + 2
number2 :: Num a => a
number2 = number1 * number1
main :: IO ()
main = print number2
Hint: don’t get caught up by the types of the values. This will probably be confusing, and should confuse you at least a little bit. We’ll explain what’s going on later, however, the important thing is just see if you can work out what the program will do when you run it.
If you’ve enjoyed reading this, please consider purchasing a copy at Leanpub today
Please follow us, and check out our videos Follow @HappyLearnTutes
Also, Volume 2 is now in beta and being written! Show your support and register your interest at its Leanpub site.