Lesson 11. Type basics

published book

After reading lesson 11, you’ll be able to

  • Understand basic types in Haskell, including Int, String, and Double
  • Read type signatures for functions
  • Use simple type variables

This lesson introduces one of the most powerful aspects of Haskell: its robust type system. The fundamentals of functional programming covered in the preceding lessons are shared by all functional programming languages from Lisp to Scala. It’s Haskell’s type system that sets it apart from other programming languages. Our introduction in this lesson starts with the basics of Haskell’s type system.

Consider this

You need to create a simple function for taking the average of a list of numbers. The most obvious solution is to take the sum of the list and divide it by the length of the list:

myAverage aList = sum aList / length aList

But this simple definition doesn’t work. How can you write a function to compute the mean of a list of numbers?

join today to enjoy all our content. all the time.
 

11.1. Types in Haskell

It may come as a bit of a surprise that Haskell is a statically typed language. Other common statically typed languages include C++, C#, and Java. In these and most other statically typed languages, the programmer is burdened with keeping track of type annotations. So far in Haskell, you haven’t had to write down any information about the type you’re using for any of your values. It turns out this is because Haskell has done it for you! Haskell uses type inference to automatically determine the types of all values at compile time based on the way they’re used! You don’t have to rely on Haskell to determine your types for you. Figure 11.1 shows a variable that you’ll give the Int type.

Figure 11.1. Type signature for a variable

All types in Haskell start with a capital letter to distinguish them from functions (which all start with a lowercase letter or _). The Int type is one of the most ubiquitous and traditional types in programming. Int represents how the computer is to interpret a number represented by a fixed number of bits, often 32 or 64 bits. Because you’re describing numbers with a fixed number of bits, you’re limited by a maximum and minimum value that a number can take on. For example, if you load x in GHCi, you can do some simple operations to show the limits of this type:

x :: Int
x = 2

GHCi> x*2000
4000
GHCi> x^2000
0

As you can see, Haskell handles exceeding the bounds of the Int by returning 0. This property of having limited maximum and minimum values is referred to as being bounded. You’ll learn more about bounded types in lesson 13.

The Int type is a traditional way of viewing types in programming. Int is a label that tells the computer how to read and understand physical memory. In Haskell, types are more abstract. They provide a way of understanding how values behave and how to organize data. For example, the Integer type more closely resembles the typical Haskell way of thinking about types. Let’s see how to define a new variable y as an Integer.

Listing 11.1. Integer type

You can clearly see the difference between these two types by repeating your calculations from before:

GHCi> y*2000
4000
GHCi> y^2000
11481306952742545242328332011776819840223177020886952004776427368257662613
923703138566594863165062699184459646389874627734471189608630553314259313561
666531853912998914531228000068877914824004487142892699006348624478161546364
638836394731702604046635397090499655816239880894462960562331164953616422197
033268134416890898445850560237948480791405890093477650042900271670662583052
200813223628129176126788331720659899539641812702177985840404215985318325154
088943390209192055495778358967203916008195721663058275538042558372601552834
878641943205450891527578388262517543552880082284277081796545376218485114902
9376

As you can see, the Integer type fits more closely with the mathematical sense of what an integer is: any whole number. Unlike the Int type, the Integer type isn’t bounded by memory limitations framed in terms of bytes.

Haskell supports all the types that you’re likely familiar with in other languages. Here are some examples.

Listing 11.2. Common types Char, Double, and Bool

Another important type is List. Here are a few examples.

Listing 11.3. List types

A list of characters is the same as a string:

GHCi> letters == "abc"
True

To make things easier, Haskell allows you to use String as a type synonym for [Char]. Both of these type signatures mean exactly the same thing to Haskell:

aPet :: [Char]
aPet = "cat"

anotherPet :: String
anotherPet = "dog"

Another important type is a Tuple. You used tuples briefly in lesson 4. When you weren’t thinking about types, tuples didn’t seem too different from a list, but they’re quite a bit more sophisticated. Two main differences are that each tuple is of a specific length, and tuples can contain multiple types. A list of type [Char] is a string of any size, whereas a tuple of type (Char) is a tuple of exactly one character. Here are some more tuple examples.

Listing 11.4. Tuple types

Tuples are useful for modeling simple data types quickly.

Get Get Programming with Haskell
add to cart

11.2. Function types

Functions also have type signatures. In Haskell an -> is used to separate arguments and return values. The type signature for double looks like figure 11.2.

Figure 11.2. Defining the double function by using a type signature

You could easily have chosen Integer, Double, or any other number of types for your argument. In lesson 12, you’ll look at type classes that allow you to generalize numbers better.

Taking in an Int and returning an Int works for doubling a number, but it doesn’t work for halving a number. If you want to write a function half and you want to take in an Int, you need to return a Double. Your type signature will look like the following.

Listing 11.5. Converting from one type to another with half

Now you need to define your function, and a first guess would be this:

But this results in an error. The problem is that you’re trying to divide a whole number Int in half, and such a thing is nonsensical because you’ve already declared that you’re going to return a Double. You need to convert your value from an Int into a Double. Most programming languages have the idea of casting a variable from one type to another. Casting forces a value to be represented as a different type. Because of this, casting variables often feels like hammering a square peg through a round hole. Haskell has no convention for casting types and instead relies on functions that properly transform values from one type to another. In this case, you can use Haskell’s fromIntegral function:

half n = (fromIntegral n) / 2

Here you’ve transformed n from an Int into a more general number. A good question now might be, “Why don’t you have to call fromIntegral on 2?” In many programming languages, if you want to treat a literal number as a Double, you need to add a decimal to it. In both Python and Ruby, 5/2 is 2 and 5/2.0 is 2.5. Haskell is both stricter and more flexible. It’s stricter because Haskell never does the implicit type conversion that happens in Ruby and Python, and it’s more flexible because in Haskell literal numbers are polymorphic: their type is determined from the compiler based on the way they’re used. For example, if you want to use GHCi as a calculator, you’ll find you rarely need to worry about type with numbers:

GHCi> 5/2
2.5
Quick check 11.1

Q1:

Haskell has a function named div that does perform integer division (it returns only whole numbers). Write halve, which uses div instead, and include a type signature.

QC 11.1 answer
halve :: Integer -> Integer
halve value = value `div` 2

11.2.1. Functions for converting to and from strings

One of the most common type conversions is to convert values to and from strings. Haskell has two useful functions that achieve this: show and read. Lessons 12 and 13 detail how these work, but for now let’s look at some examples in GHCi. The show function is straightforward:

GHCi> show 6
"6"
GHCi> show 'c'
"'c'"
GHCi>show 6.0
"6.0"
Quick check 11.2

Q1:

Write a function printDouble that takes an Int and returns that value doubled as a string.

QC 11.2 answer
printDouble :: Int -> String
printDouble value = show (value*2)

The read function works by taking a string and converting it to another type. But this is a bit trickier than show. For example, without type signatures, what should Haskell do here?

z = read "6"

It’s impossible to tell whether to use Int, Integer, or even Double. If you can’t figure it out, there’s absolutely no way that Haskell can. In this case, type inference can’t save you. There are a few ways to fix this. If you use the value z, it’s likely that Haskell will have enough info to figure out how to treat your value:

q = z / 2

Now Haskell has enough information to treat z like a Double, even though your String representation didn’t have a decimal. Another solution is to explicitly use your type signature.

Listing 11.6. Example of reading values from strings: anotherNumber

Even though you got through the first unit with no type signatures, it’s generally a good idea to always use them. This is because in practice type signatures help you reason about the code you’re writing. This little extra annotation lets Haskell know what you expect read to do and makes your own intentions clearer in the code. There’s one more way to force Haskell to understand what type you want that comes up often in practice. You can always append the expected return type to the end of a function call. This happens most frequently in GHCi, but at other times it’s helpful to specify an ambiguous return type:

GHCi> read "6" :: Int
6
GHCi> read "6" :: Double
6.0

11.2.2. Functions with multiple arguments

So far, most of your type signatures have been straightforward. One thing that frequently trips up newcomers to Haskell is the type signature for multi-argument functions. Suppose you want a function that takes a house number, street address, and town and makes a tuple representing an address. Figure 11.3 shows the type signature.

Figure 11.3. Type signature for multi-argument functions and definition makeAddress

What makes this confusing is that there’s no clear separation between which types are for arguments and which are for return values. The easy way to remember is that the last type is always the return type. A good question is, why are type signatures this way? The reason is that behind the scenes in Haskell, all functions take only one argument. By rewriting makeAddress by using a series of nested lambda functions, as shown in figure 11.4, you can see a multi-argument function the way Haskell does.

Figure 11.4. Desugaring the multi-argument makeAddress into a sequence of single-argument functions

You could then call this function like so:

GHCi> (((makeAddressLambda 123) "Happy St") "Haskell Town")
(123,"Happy St","Haskell Town")

In this format, each function returns a function waiting for the next. This might seem crazy until you realize this is how partial application works! You could apply arguments in exactly the same way with makeAddress and get the exact same results:

GHCi> (((makeAddress 123) "Happy St") "Haskell Town")
(123,"Happy St","Haskell Town")

It also turns out that because of the way Haskell evaluates arguments, you can call your desugared lambda version the way you would any ordinary function:

GHCi>makeAddressLambda 123 "Happy St" "Haskell Town"
(123,"Happy St","Haskell Town")
Quick check 11.3

Q1:

As each argument is passed to makeAddress, write out the type signature of the returned function.

QC 11.3 answer

Starting with our type original type signature:

makeAddress :: Int -> String -> String -> (Int,String,String)

And your type signatures is now as follows:

String -> String -> (Int,String,String)

Then pass in the first String:

((makeAddress 123) "Happy St")

And here’s the type signature:

String -> (Int,String,String)

Finally, if you pass in all of your arguments, you get the type of the result:

(((makeAddress 123) "Happy St") "Haskell Town")
(Int,String,String)

Hopefully, this helps to demystify multi-argument type signatures as well as partial application!

11.2.3. Types for first-class functions

As we mentioned in lesson 4, functions can take functions as arguments and return functions as values. To write these type signatures, you write the individual function values in parentheses. For example, you can rewrite ifEven with a type signature.

Listing 11.7. Type signatures for first-class functions: ifEven
Sign in for more free preview time

11.3. Type variables

We’ve covered a bunch of common types and how they work in functions. But what about the simple function, which returns any value that’s passed in to it? Really, simple could take any type of argument at all. Given what you know so far, you’d have to make a family of simple functions to work with every type.

Listing 11.8. simpleInt and

But this is ridiculous, and clearly not how Haskell works, because type inference was able to understand simple. To solve this problem, Haskell has type variables. Any lowercase letter in a type signature indicates that any type can be used in that place. The type definition for simple looks like the following.

Listing 11.9. Using type variables: simple

Type variables are literally variables for types. Type variables work exactly like regular variables, but instead of representing a value, they represent a type. When you use a function that has a type variable in its signature, you can imagine Haskell substituting the variable that’s needed, as shown in figure 11.5.

Figure 11.5. Visualizing type variables taking on actual values

Type signatures can contain more than one type of variable. Even though the types can be any value, all types of the same variable name must be the same. Here’s an example of a function that makes triples (tuples with three values).

Listing 11.10. Multiple type variables: makeTriple

The reason for different names for type variables is the same as using different names for regular variables: they may contain different values. In the case of makeTriple, you can imagine a case in which you have a String, a Char, and another String:

nameTriple = makeTriple "Oscar" 'D' "Grouch"

In this example, you can imagine that the type signature that Haskell uses looks like this:

makeTriple :: String -> Char -> String -> (String, Char, String)

Notice that the definition of makeTriple and makeAddress are nearly identical. But they have different type signatures. Because of makeTriple’s use of type variables, makeTriple can be used for a more general class of problems than makeAddress. For example, you could use makeTriple to replace makeAddress. This doesn’t render makeAddress useless. Because makeAddress has a more specific type signature, you can make more assumptions about how it behaves. Additionally, Haskell’s type checker won’t allow you to create an address where you accidently used a String for the number instead of an Int.

Just as with regular variables, using different names for type variables doesn’t imply that the values represented by the variables must be different, only that they can be. Say you compare the type signatures of two unknown functions f1 and f2:

f1 :: a -> a
f2 :: a -> b

You know that f2 is a function that can produce a much wider range of possible values. The f1 function could behave only by changing a value and keeping it as the same type: Int -> Int, Char -> Char, and so forth. In contrast, f2 can represent a much broader range of possible behaviors: Int -> Char, Int -> Int, Int -> Bool, Char -> Int, Char -> Bool, and so forth.

Quick check 11.4

Q1:

The type signature for map is as follows:

map :: (a -> b) -> [a] -> [b]

Why couldn’t it be this?

map :: (a -> a) -> [a] -> [a]?

Hint: Fill in the type variables for myMap show [1,2,3,4].

QC 11.4 answer

map:: (a -> a) -> [a] -> [a] would mean that map must always return the same type as it currently is.

In this case, you couldn’t perform

map show [1,2,3,4]

because show returns a type String that isn’t consistent with the original type. The real power of map isn’t iteration, but transforming a list of one type into a list of another type.

Summary

In this lesson, our objective was to teach you the basics of Haskell’s amazing type system. You saw that Haskell has many of the standard types that programmers are familiar with, such as Int, Char, Bool, and String. Despite Haskell’s powerful type system, you were able to get this far in the book without explicitly using types because of Haskell’s type inference, which allows Haskell to figure out the types you intend by how they’re used. Even though Haskell can often handle your code without types, writing down type signatures turns out to be much more beneficial for the programmer. From this point in the book onward, most of our discussion will typically come back to “thinking in types.” Let’s see if you got this.

What is the type signature for filter? How is it different from map?

In Haskell, both tail and head have an error when called on an empty list. You can write a version of tail that won’t fail but instead return an empty list when called on an empty list. Can you write a version of head that returns an empty list when called on an empty list? To answer this, start by writing out the type signatures of both head and tail.

Recall myFoldl from lesson 9.

myFoldl f init [] = init
myFoldl f init (x:xs) = myFoldl f newInit xs
  where newInit = f init x

What’s the type signature of this function? Note: foldl has a different type signature.

sitemap
×

Unable to load book!

The book could not be loaded.

(try again in a couple of minutes)

manning.com homepage