Lesson 2. Functions and functional programming

After reading lesson 2, you’ll be able to

  • Understand the general idea of functional programming
  • Define simple functions in Haskell
  • Declare variables in Haskell
  • Explain the benefits of functional programming

The first topic you need to understand when learning Haskell is, what is functional programming? Functional programming has a reputation for being a challenging topic to master. Although this is undoubtedly true, the foundations of functional programming are surprisingly straightforward. The first thing you need to learn is what it means to have a function in a functional programming language. You likely already have a good idea of what using a function means. In this lesson, you’ll see the simple rules that functions must obey in Haskell that not only make your code easier to reason about, but also lead to entirely new ways of thinking about programming.

Consider this

You and your friends are out getting pizza. On the menu are three sizes of pizza pie with three different prices:

  1. 18 inches for $20
  2. 16 inches for $15
  3. 12 inches for $10

You want to know which choice gives you the most pizza for your dollar. You want to write a function that will give you the dollar-per-square-inch cost of the pizza.

2.1. Functions

What exactly is a function? This is an important question to ask and understand if you’re going to be exploring functional programming. The behavior of functions in Haskell comes directly from mathematics. In math, we often say things like f(x) = y, meaning there’s some function f that takes a parameter x and maps to a value y. In mathematics, every x can map to one and only one y. If f(2) = 2,000,000 for a given function f, it can never be the case that f(2) = 2,000,001.

The thoughtful reader may ask, “What about the square-root function? 4 has two roots, 2 and –2, so how can sqrt x be a true function when it clearly points to two ys!” The key thing to realize is that x and y don’t have to be the same thing. We can say that sqrt x is the positive root, so both x and y are positive real numbers, which resolves this issue. But we can also have sqrt x be a function from a positive real number to pairs of real numbers. In this case, each x maps to exactly one pair.

In Haskell, functions work exactly as they do in mathematics. Figure 2.1 shows a function named simple.

Figure 2.1. Defining a simple function

The simple function takes a single argument x and then returns this argument untouched. Notice that unlike many other programming languages, in Haskell you don’t need to specify that you’re returning a value. In Haskell, functions must return a value, so there’s never a need to make this explicit. You can load your simple function into GHCi and see how it behaves. To load a function, all you have to do is have it in a file and use :load <filename> in GHCi:

GHCi> simple^2
2
GHCi> simple "dog"
"dog"
Note

In this section, we’re using GHCi—Haskell’s Interactive Read-Eval-Print Loop (REPL)—to run commands and get results.

All functions in Haskell follow three rules that force them to behave like functions in math:

  • All functions must take an argument.
  • All functions must return a value.
  • Anytime a function is called with the same argument, it must return the same value.

The third rule is part of the basic mathematical definition of a function. When the rule that the same argument must always produce the same result is applied to function in a programming language, it’s called referential transparency.

2.2. Functional programming

If functions are just mappings from a bunch of xs (that’s the plural of x—“exes”) to a bunch of ys (that’s the plural of y—“whys”) what do they have to do with programming? In the 1930s, a mathematician named Alonzo Church attempted to create a system of logic that used only functions and variables (xs and ys). This system of logic is called lambda calculus. In lambda calculus, you represent everything as functions: true and false are functions, and even all the integers can be represented as functions.

Church’s goal was initially to resolve some problems in the mathematical field of set theory. Unfortunately, lambda calculus didn’t solve these problems, but something much more interesting came out of Church’s work. It turns out that lambda calculus allows for a universal model of computation, equivalent to a Turing machine!

What is a Turing machine?

A Turing machine is an abstract model of a computer developed by the famous computer scientist Alan Turing. From a theoretical standpoint, the Turing machine is useful because it allows you to reason about what can and can’t be computed, not just on a digital computer, but any possible computer. This model also allows computer scientists to show equivalence between computing systems if they can each simulate a Turing machine. You can use this to show, for example, that there’s nothing that you can compute in Java that you can’t also compute in assembly language.

This discovery of the relationship between lambda calculus and computing is called the Church-Turing thesis (for more information, see www.alanturing.net/turing_archive/pages/reference%20articles/The%20Turing-Church%20Thesis.html). The wonderful thing about this discovery is that you have a mathematically sound model for programming!

Most programming languages that you use are marvelous pieces of engineering but provide little assurance about how programs will behave. With a mathematical foundation, Haskell is able to remove entire classes of bugs and errors from the code you write. Cutting-edge research in programming languages is experimenting with ways to mathematically prove that programs will do exactly what you expect. Additionally, the nonmathematical nature of most programming language designs means the abstractions you can use are limited by engineering decisions in the language. If you could program math, you’d be able to both prove things about your code and have access to the nearly limitless abstractions that mathematics allows. This is the aim of functional programming: to bring the power of mathematics to the programmer in a usable way.

2.3. The value of functional programming in practice

This mathematical model for programming has a variety of practical implications. Because of the simple rules that all functions must take and return values, and must always return the same value for the same argument, Haskell is a safe programming language. Programs are safe when they always behave exactly the way you expect them to and you can easily reason about their behavior. A safe programming language is one that forces your programs to behave as expected.

Let’s look at code that isn’t safe and violates our simple rules for functions. Suppose you’re reading through a new code base and you come across lines of code that look like the following.

Listing 2.1. Hidden state in function calls
tick()
if(timeToReset){
  reset()
}

This code clearly isn’t Haskell, because both tick and reset violate the rules we established. Neither function takes any arguments nor returns any value. The question is, then, what are these functions doing, and how is this different from functions in Haskell? It’s not a long shot to suppose that tick is incrementing a counter and that reset restores that counter to its starting value. Even if we’re not exactly right, this reasoning gives us insight into our question. If you aren’t passing an argument to a function, you must be accessing a value in your environment, and if you aren’t returning a value, you must also be changing a value in your environment. When you change a value in your programming environment, you’re changing the program’s state. Changing state creates side effects in your code, and these side effects can make code hard to reason about and therefore unsafe.

It’s likely that both tick and reset are accessing a global variable (a variable reachable from anywhere in the program), which in any programming language is considered poor design. But side effects make it hard to reason about even the simplest, well-written code. To see this, you’ll look at a collection of values, myList, and reverse it by using built-in functionality. The following code is valid Python, Ruby, and JavaScript; see if you can figure out what it does.

Listing 2.2. Confusing behavior in standard libraries
myList = [1,2,3]
myList.reverse()
newList = myList.reverse()

Now what do you expect the value of newList to be? Because this is a valid program in Ruby, Python, and JavaScript, it seems reasonable to assume that the value of newList should be the same. Here are the answers for all three languages:

Ruby -> [3,2,1]
Python -> None
JavaScript -> [1,2,3]

Three completely different answers for the exact same code in three languages! Python and JavaScript both have side effects that occur when reverse is called. Because the side effects of calling reverse are different for each language and aren’t made visible to the programmer, both languages give different answers. The Ruby code here behaves like Haskell, without side effects. Here you see the value of referential transparency. With Haskell, you can always see which effects each function has. When you called reset and tick earlier, the changes they made were invisible to you. Without looking at the source code, you have no way of knowing exactly which or even how many other values they’re using and changing. Haskell doesn’t allow functions to have side effects, which explains why all Haskell functions must take an argument and return a value. If Haskell functions didn’t always return a value, they’d have to change a hidden state in the program; otherwise, they’d be useless. If they didn’t take an argument, they’d have to access a hidden one, which would mean they’re no longer transparent.

This small property of Haskell’s functions leads to code that’s dramatically easier to predict. Even in Ruby, the programmer is allowed to use side effects. When using another programmer’s code, you still can’t assume anything about what’s happening when you call a function or method. Because Haskell doesn’t allow this, you can look at any code, written by any programmer, and reason about its behavior.

Quick check 2.1

Q1:

Many languages use the ++ operator to increment a value; for example, x++ increments x. Do you think Haskell has an operator or function that works this way?

QC 2.1 answer

1:

The ++ operator used in languages such as C++ couldn’t exist in Haskell because it violates our mathematical rules for functions. The most obvious rule is that each time you call ++ on a variable, the result is different.

 

2.3.1. Variables

Variables in Haskell are straightforward. Here you’re assigning 2 to the variable x.

Listing 2.3. Defining your first variable
x = 2

The only catch with variables in Haskell is that they’re not really variable at all! If you were to try to compile the following bit of Haskell, you’d get an error, as shown in the next listing.

Listing 2.4. Variables aren’t variable!
x = 2
x = 3           1

  • 1 Won’t compile because it changes the value of x

A better way to think about variables in Haskell is as definitions. Once again, you see mathematical thinking replace the way you typically think about code. The problem is that in most programming languages, variable reassignment is essential to solving many problems. The inability to change variables is also related to referential transparency. This may seem like a strict rule to follow, but the reward is that you always know that after calling a function, the world remains the same.

Quick check 2.2

Q1:

Even languages that don’t have a ++ operator allow for a += operator, often also used for incrementing a value. For example, x += 2 increments x by 2. You can think of += as a function that follows our rules: it takes a value and returns a value. Does this mean += can exist in Haskell?

QC 2.2 answer

1:

Although the += operator returns and takes an argument, just like ++, every time you call +=, you get a different result.

 

The key benefit of variables in programming is to clarify your code and avoid repetition. For example, suppose you want a function called calcChange. This function takes two arguments: how much is owed and how much is given. If you’re given enough money, you return the difference. But if you aren’t given enough money, you don’t want to give negative dollars; you’ll return 0. Here’s one way to write this.

Listing 2.5. calcChange v.1
calcChange owed given = if given - owed > 0
                        then given - owed
                        else 0

Two things are wrong with this function:

  • Even for a tiny function, it’s hard to read. Each time you see the expression given - owed, you have to reason about what’s happening. For anything more complicated than subtraction, this would be unpleasant.
  • You’re repeating your computation! Subtraction is a cheap operation, but if this had been a costlier operation, you’d be needlessly wasting resources.

Haskell solves these problems by using a special where clause. Here’s the previous function written with a where clause.

Listing 2.6. calcChange v.2
calcChange owed given = if change > 0
                        then change
                        else 0
  where change = given – owed               1

  • 1 given – owed is computed only once and assigned to change.

The first thing that should strike you as interesting is that a where clause reverses the normal order used to write variables. In most programming languages, variables are declared before they’re used. This convention in most programming languages is partially the byproduct of being able to change state. Variable order matters because you can always reassign the value of something after you’ve assigned it. In Haskell, because of referential transparency, this isn’t an issue. There’s also a readability gain with the Haskell approach: if you read the algorithm, the intention is clear right away.

Quick check 2.3

Q1:

Fill in the missing part of the following where clause:

doublePlusTwo x = doubleX + 2
   where doubleX = __________

QC 2.3 answer

1:

doublePlusTwo x = doubleX + 2
  where doubleX = x*2

 

2.3.2. Variables that are variable

Because change is an inevitable part of life, sometimes it makes sense to have variables that can be reassigned. One of these cases occurs when working in the Haskell REPL, GHCi. When working in GHCi, you’re allowed to reassign variables. Here’s an example:

GHCi> x = 7
GHCi> x
7
GHCi> x  = [1,2,3]
GHCi> x
[1,2,3]

Prior to version 8 of GHC, variables in GHCi needed to be prefaced with the let keyword to mark them as different from other variables in Haskell. You can still define variables by using let in GHCi if you like:

GHCi> let x = 7
GHCi> x
7

It’s also worth noting that one-line functions can be defined in the same way:

GHCi> let f x = x^2
GHCi> f 8
64

In a few other special contexts in Haskell, you’ll see let used in this way. It can be confusing, but this difference is primarily to make real-world tasks less frustrating.

It’s important to acknowledge that being able to change the definition of variables in GHCi is a special case. Although Haskell may be strict, having to restart GHCi every time you wanted to experiment with a different variable would be frustrating.

Quick check 2.4

Q1:

What’s the final value of the x variable in the following code?

GHCi> let x = simple simple
GHCi> let x = 6

QC 2.4 answer

1:

Because you can reassign values, the final value of x is 6.

 

Summary

In this lesson, our objective was to introduce you to functional programming and writing basic functions in Haskell. You saw that functional programming puts restrictions on the behavior of a function. These restrictions are as follows:

  • A function must always take an argument.
  • A function must always return a value.
  • Calling the same function with the same argument must always return the same result.

These three rules have profound consequences for the way you write programs in Haskell. The major benefit of writing code in this style is that your programs are much easier to reason about, and behave predictably. Let’s see if you got this.

Q2.1

You used Haskell’s if then else expression to write calcChange. In Haskell, all if statements must include an else component. Given our three rules for functions, why can’t you have an if statement all by itself?

Q2.2

Write functions named inc, double, and square that increment, double, and square an argument n, respectively.

Q2.3

Write a function that takes a value n. If n is even, the function returns n - 2, and if the number is odd, the function returns 3 × n + 1. To check whether the number is even, you can use either Haskell’s even function or mod (Haskell’s modulo function).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.62.211