A First Look at Haskell
We take our first look at functional programming in Haskell by using it to solve a simple problem. We also compare the differences to using an imperative language.
We'll cover the following
What is Haskell?
Haskell is a compiled, statically typed, functional programming language. It was named after the American logician Haskell Brooks Curry.
- Compiled means it is not run directly by an interpreter. Instead, GHC (the Glorious Haskell Compiler) translates our programs into machine code that the computer executes directly.
- Statically typed means that every variable in a program has a fixed type (e.g. real number, string of characters). The type is known in advance, either by manually specifying the type, or by automatically inferring it.
- Functional specifies the programming language paradigm. As the name suggests, functional programs are all about functions, in particular, so-called pure functions that have no side effects. Pure functions are easy to compose, which supports building complex programs from simple parts.
The functional paradigm is an instance of declarative programming. It separates Haskell from most other programming languages that follow the imperative paradigm, which is all about giving the computer a sequence of instructions on how to solve a problem.
A first example
To illustrate the difference, let’s look at a simple example problem and solve it in both Python and Haskell. Let’s say we have 1000$ in our bank account and want to invest it. Our bank is offering us an investment plan with a yearly 5% interest rate. How much money will we have after n years have passed?
Imperative solution
An imperative style Python program for this calculation could look like this:
def compound_interest(years):current_money = 1000for year in range(years):current_money = current_money * 1.05print('After {years} years, you have {money:.2f} dollars.'.format(years=years, money=current_money))return current_moneycompound_interest(10)
It showcases some typical features of imperative programs:
- The program is a sequence of statements that are executed one after the other.
- Statements can have side effects, i.e., they affect something outside of the scope of the function. In this example, there is a
print
statement which prints the output of the calculation to the user’s screen. - There is explicit control flow, such as a loop statement to iterate over the years and a return statement to signify the end of the function.
- A variable is declared (
current_money
). The variable is mutable, i.e. it can be modified. And it is in fact modified several times during the loop until its value is returned in the end.
If you are familiar with imperative programming, all of these things will be second nature to you. In fact, it might be hard to envision solving this problem in any other way. Now, let’s see how to solve this problem in Haskell, a language without statements, loops, or mutable variables.
Rethinking the problem
Before we look at the solution, let’s take a step back from this imperative solution. Declarative programming encourages us to define the problem in a more abstract way, independent of the concrete order of instructions. Often, it is helpful to express the problem through a recursive equation.
- At the beginning, (after “0 years”) our investment is 1000 dollars.
- After one year, our investment is
1.05 * 1000 = 1050
dollars. - After two years, our investment is
1.05 * 1050 = 1102.5
dollars. We can rewrite this as1.05 * (1.05 * 1000)
, where(1.05 * 1000)
is the formula for one year. - So, if we invest our money for
n
years, we have to multiply the solution for (n-1
) years with 1.05.
This gives us the recursive formula:
Functional solution
Our Haskell program is a direct translation of this recursive mathematical definition:
compoundInterest :: Int -> DoublecompoundInterest 0 = 1000compoundInterest n = 1.05 * (compoundInterest (n - 1))main = printf "After 10 years, you have %.2f dollars." (compoundInterest 10)
Let’s go over this line by line. The first line:
compoundInterest :: Int -> Double
is a type annotation for our function called compoundInterest
. Unlike Python, Haskell is statically typed, and every function or variable has a type. In this case, compoundInterest
is a function that takes an integer (the number of years) as its argument and returns a double (a floating point number).
The next two lines are the function definition. It consists of two equations that implement our recursive formulation of the problem.
compoundInterest 0 = 1000
This solves the base case where we invest our money for zero years. The 0
here represents the integer argument to our function. In this way, we express in Haskell that the equation can only be used when the argument is 0
.
compoundInterest n = 1.05 * (compoundInterest (n - 1))
This solves the case where we invest for n
years. It recursively calls compoundInterest
for (n - 1
) years and multiplies the result with 1.05
. By using the variable name n
instead of a concrete number like 0
, we express that this equation can be used for any value of the integer argument in our function. Remember, this equation will not be used when n
is 0
, as the first equation already covered that case.
The last line is the special main
function that prints the result.
In contrast to the imperative program:
- There are type and static type annotations.
- There are no statements. Instead, the function is defined case by case with expressions.
- There is no loop. Instead, we use recursive calls to multiply the interest rate again and again.
- There is no mutable variable. Instead, we use a recursive expression to obtain the value after
n
years from the value after (n - 1
) years. - There are no side effects inside this function (printing the result to the screen happens outside of the function that computes the result).
This concludes our first look at approaching problems using Haskell. It takes some time to get used to the declarative style of problem solving, but there are benefits to mastering this approach. Later in the course we will take a closer look at the topics we glossed over, such as recursion, the type system, and how side effects are handled in Haskell.