Haskell 101: Installation, Expressions and Types
Welcome to part 1 of the Monday Morning Haskell Liftoff Series! If you've always wanted to try learning Haskell but were never able to find a good tutorial for it, you're in the right place! You might have zero knowledge about this awesome language right now. But after these three articles, you should know the basic concepts well enough that you can start programming on your own.
This article will cover a few different topics. First we'll download all the tools we need and install them. Then we'll start writing our first expressions and learning a bit about Haskell's type system. Next, we'll put the "function" in functional programming and learn how Haskell's functions are first class citizens. Finally, we'll wrap up by talking about some slightly more complicated types like lists and tuples.
If you've already read this article or are familiar with all these concepts, you should jump straight to part 2. There, we'll talk about writing our own code files, and writing more complicated functions with some advanced syntax. Make sure you also don't miss part 3, where we'll look at how easy it is to create our own data types!
This series also comes with a companion Github repository! This repository will allow you to work with some of the code samples from these articles. In this first part though, we'll mostly be working within GHCI, rather than source code files.
Finally, once you're done with these, make sure to check out our Beginner's Checklist! This will provide you with a review of the series and guide you to many more resources for improving your Haskell!
Installation
If you have not touched any Haskell whatsoever, the first step is to download the Haskell Platform. Download the latest version for your operating system from and follow the on-screen prompts.
The platform currently includes 4 main items. First, it has GHC, the most commonly used Haskell compiler. The compiler is what takes your code and turns it into something the computer can run. Second, it has GHCI, an interpreter for the Haskell language. It allows you to enter expressions and test some calculations without having to put your code in a separate file.
Third, it includes "Cabal", a dependency manager for Haskell libraries. This will allow you to download code that other people have written and use it in your own projects. Finally, there is the "Stack" tool. This adds another layer on top of Cabal and makes it easier for you to download packages that won't cause conflicts. If you want more details about Stack, you can look at our Stack Mini-Course!
To test if you've installed everything correctly, see if the command ghci
works in your terminal and brings up the interpreter. We'll spend the rest of this lesson in GHCI trying out some of Haskell's basic language features.
Expressions
Now that you've installed everything, let's get down to the basics! The most fundamental thing about Haskell is that everything you write is an expression. All your program actually consists of is evaluating these expressions. Let's start by examining some of the most basic expressions we can make. Try entering the following 6 expressions in the interpreter. When you press enter each time, the interpreter should simply echo back what you typed.
>> True
True
>> False
False
>> 5
5
>> 5.5
5.5
>> 'a'
'a'
>> "Hello"
"Hello
With this series of expressions, we cover most of the basic "types" of the language. If you've done any programming in the past, these basic types should be pretty familiar to you. The first two are "boolean" expressions. True and False are actually the only values of this type. We can also make expressions out of numbers, both whole and decimal. Finally, we can make expressions representing individual characters as well as whole words, which we call strings.
In the interpreter, we can assign expressions to names by using "let" and an equals sign. This saves the expression under that name so we can refer to it later.
>> let firstString = "Hello"
>> firstString
"Hello"
Types
Now, one of the cool things about Haskell is that every expression has a type. Let's examine the types of the basic expressions we entered above. We'll see that the ideas we were talking about are formalized in the language itself. You can examine the type of any expression by using the :t
command:
>> :t True
True :: Bool
>> :t False
False :: Bool
>> :t 5
5 :: Num t => t
>> :t 5.5
5.5 :: Fractional t => t
>> :t 'a'
'a' :: Char
>> :t "Hello"
"Hello" :: [Char]
A couple of these are straightforward, but a couple are kind've weird. Isn't that last one just a String
? Well yes. You can use the term String
in your code. Under the hood though, Haskell thinks of Strings as a list of characters, which is what [Char]
means. We'll get to that later. True and False correspond to the Bool
type, just like we'd expect. The 'a'
character is a single Char
. Our numbers are a little more complicated. Ignore the Num
and Fractional
words for now. This is how we can refer to a whole range of types. We'll think of whole numbers as having type Int
, and floating point numbers as having type Double
. We can explicitly assign the type we like like so:
>> let a = 5 :: Int
>> :t a
a :: Int
>> let b = 5.5 :: Double
>> :t b
b :: Double
We can already see something pretty cool about Haskell. It can infer some information about the types of our expressions just from their form. We generally don't need to explicitly give a type to each of our expressions like we do in a language like Java or C++ (think int a = ...
).
Functions
Let's start doing some computations with our new expressions and see what we come up with. We can start with some basic mathematical calculations:
>> 4 + 5
9
>> 10 - 6
4
>> 3 * 5
15
>> 3.3 * 4
13.2
>> (3.3 :: Double) * (4 :: Int)
Once again, we've been thrown a curveball by this last example! By the time we're done with this section, we'll understand what's going on here and how we can fix it. Now, an important note here is that we said everything in Haskell is an expression, and every expression has a type. So logically, we should be able to ask and determine the types of these different expressions. And we certainly can! We just have to put parentheses around them to make sure the type command knows to include the whole expression.
>> let a = 4 :: Int
>> let b = 5 :: Int
>> a + b
9
>> :t (a + b)
(a + b) :: Int
This makes perfect sense, since the result, 9
, seems like an Int
as well. But this is where it gets cool. The +
operator, even on its own without the numbers, is still an expression! It is our first example of a function, or an expression that takes arguments. When we represent it by itself, we put parentheses around it.
>> :t (+)
(+) :: Num a => a -> a -> a
This is our first example of representing the type of a function. The important part is a -> a -> a
. This tells us that (+)
is a function taking two arguments, which must have the same type. It will then give us a result that has the same type as the inputs. The Num a
portion specifies we have to use numeric types, like ints and floats. We can't for instance do:
>> "Hello " + "World"
But this explains why we were not able to add an int and a double together. The function demands we use the same type for both arguments. To fix this, we'd have to use a different function to change the type of one of them to match the other. Or we could let type inference figure it out, like we had in the example right above it. But we're getting ahead of ourselves. Let's focus on the semantics of how we "apply" this function.
In general, we "apply" functions by placing the arguments after the functions. The +
function is special in that we can use it as an operator in between its arguments. If we want though, we can use parentheses around it and treat it like a normal function. In this case, both arguments need to come after it:
>> (+) 4 5
9
What's really cool about functions is that we don't have to apply all the arguments at once!. We can take the same addition operator and apply it to only one of the numbers. This is called partial application.
>> let a = 4 :: Int
>> :t (a +)
(a +) :: Int -> Int
On its own, (+) was an operator that could take two arguments. Now we've applied one argument (an Int
) to it. The resulting expression is now a function that takes one remaining argument. Furthermore, since the first argument we passed was an Int
, the second one must also be an Int
. We can assign our partial function to an expression using let
and then apply it to a second argument!
>> let f = (4 +)
>> f 5
9
Let's experiment a bit with some more operators, this time on boolean values. These are important because they'll let you build more advanced conditions when you start writing functions. There are three main operators, which work the way you would expect for other languages: And, Or, and Not. The first two take two boolean parameters and return a boolean, and the last takes a single boolean and returns it.
>> :t (&&)
(&&) :: Bool -> Bool -> Bool
>> :t (||)
(||) :: Bool -> Bool -> Bool
>> :t not
not :: Bool -> Bool
And we can see some simple examples of their behavior:
>> True && False
False
>> True && True
True
>> False || True
True
>> not True
False
One final function we'll want to know about is the equality function. This can take two arguments of almost any type and determine if they are equal.
>> 5 == 5
True
>> 4.3 == 4.8
False
>> True == False
False
>> "Hello" == "Hello"
True
Lists
Now we're going to broaden our horizons a bit and discuss some more advanced types. The first concept we'll look at is lists. These are a series of values that have the same type. We denote lists by using square brackets. A list can have zero elements, and we call this the empty list.
>> :t [1,2,3,4,7]
[1,2,3,4,7] :: Num t -> [t]
>> :t [True, False, True]
[True, False, True] :: [Bool]
>> :t ["Hello", True]
Error! (these aren't the same type!)
>> :t []
[] :: [t]
Notice the error in the third example! Lists can't have different types of elements! Remember we said earlier that a string is really just a list of characters. We can test how this looks:
>> "Hello" == ['H', 'e', 'l', 'l', 'o']
True
Lists can be combined using the (++)
operator. Because strings are lists, this allows you to combine strings like you can in other languages.
>> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]
>> "Hello " ++ "World"
"Hello World"
Lists also have two functions that are specially designed to get certain elements out. We can use the head
function to get the first element of the list. Similarly, we can use the tail
function to get all the elements of a list EXCEPT the head element.
>> head [1,2,3]
1
>> tail [True, False, False]
[False, False]
>> tail [3]
[]
Beware though! Calling either of these functions on an empty list will result in an error!
>> head []
Error!
>> tail []
Error!
Tuples
So now that we know about lists, you might be wondering if there's any way we can combine elements that do not have the same type. In fact there is! These are called tuples! You can make tuples that have any number of elements, each with different types. Tuples are denoted using parentheses:
>> :t (1 :: Int, "Hello", True)
(1 :: Int, "Hello", True) :: (Int, [Char], Bool)
>> :t (1 :: Int, 2 :: Int)
(1 :: Int, 2 :: Int) :: (Int, Int)
Each tuple we make has its own type based on the constituent types within the tuple. This means the following are all different types, even though some of them share the same types of elements, or have the same length:
>> :t (1 :: Int, 2 :: Int)
(1 :: Int, 2 :: Int) :: (Int, Int)
>> :t (2 :: Int, 3 :: Int, 4 :: Int)
(2 :: Int, 3 :: Int, 4 :: Int) :: (Int, Int, Int)
>> :t ("Hi", "Bye", "Good")
([Char], [Char], [Char])
Since tuples are expressions like any other, we can make lists out of them! However, we cannot combine tuples of different types in the same list.
>> :t [(1 :: Int, 2 :: Int), (3 :: Int, 4 :: Int)]
[(1 :: Int, 2 :: Int), (3 :: Int, 4 :: Int)] :: [(Int, Int)]
>> :t [(True, False, True), (False, False, False)]
[(Bool, Bool, Bool)]
>> :t [(1,2), (1,2,3)]
Error
Conclusion
This concludes the first section of our liftoff series. Look at how much we've gone through in one article! We installed the Haskell platform and started experimenting with the GHCI, a code interpreter. We also learned about expressions, types, and functions, which are the building blocks of Haskell.
In part 2 of this series, we'll start writing our own code in Haskell source files and learn more about the language syntax. We'll examine how we can print output to a user of our program, as well as get their input. We'll also start writing our own functions and look at the various ways we have to specify function behavior.
Then in part 3, we'll start building our own data types. We'll see how simple Haskell's algebraic data types are, and how type synonyms and newtypes can give us additional control over the style of our code.
If you want some more resources for learning introductory Haskell, check out our Beginner's Checklist! It'll tell you about some cool tools and other great beginner resources! You'll also get a great summary of the key points of this Liftoff series!
And if you're itching to do some more advanced Haskell work, be sure to check out our Stack Mini Course! This will walk you through creating a basic Haskell program with the Stack utility! You'll be able to seamlessly incorporate code libraries from online and build your own applications piece by piece!