Lesson 4. First-class functions

After reading lesson 4, you’ll be able to

  • Understand the definition of first-class functions
  • Use functions as arguments to other functions
  • Abstract computation out of a function
  • Return functions as values

Although functional programming has long had the reputation of being overly academic, nearly all of the key features of functional programming languages are starting to appear in many other more mainstream programming languages. The most widespread of these features is that of first-class functions. These are functions that can be passed around like any other values. A decade ago, this idea was shocking to many programmers, but today the majority of programming languages support and frequently use this concept. If you’ve ever assigned an event handler in JavaScript or passed custom sort logic into a sort method in a language such as Python, you’ve already used first-class functions.

Consider this

Suppose you want to create a website that compares prices of various items on other sites such as Amazon and eBay. You already have a function that returns the URL of the item you need, but you need to write code for each site that determines how to extract the price from the page. One solution is to make a custom function for each site:

getAmazonPrice url
getEbayPrice url
getWalmartPrice url

This would be fine, except all of these functions share a lot of logic (for example, parsing a string price such as $1,999.99 into a numeric type such as 1999.99). Is there a way to entirely separate the logic that extracts the price from the HTML and pass that into a common getPrice function?

4.1. Functions as arguments

The concept of first-class functions is that functions are no different from any other data used in a program. Functions can be used as arguments and returned as values from other functions. This is a deceptively powerful feature for a programming language to have. It allows you to abstract out any repetitive computation from your code, and ultimately allows you to write functions that write other functions.

Suppose you have a function ifEveninc that increments a number n if it’s even; otherwise, it returns the number unchanged, as the next listing shows.

Listing 4.1. ifEvenInc
ifEvenInc n = if even n
              then n + 1
              else n

Later you find out that you need two more functions, ifEvenDouble and ifEvenSquare, which double and square even numbers, respectively, as shown next. These are easy functions to write, given that you know how to write ifEveninc.

Listing 4.2. ifEvenDouble and ifEvenSquare
ifEvenDouble n = if even n
                 then n * 2
                 else n

ifEvenSquare n = if even n
                 then n^2
                 else n

Although these functions are easy to write, all three are nearly identical. The only difference is in the behavior of incrementing, doubling, and squaring. What you’ve discovered here is a general pattern of computation that you can abstract away. The key thing you need to do this is the ability to pass a function as an argument to perform the desired behavior.

Let’s demonstrate this with the function ifEven, which takes a function and a number as arguments. If that number is even, ifEven applies a function to that number.

Listing 4.3. ifEven
ifEven myFunction x = if even x
                      then myFunction x
                      else x

You can also abstract out your incrementing, doubling, and squaring behavior into three separate functions:

inc n = n + 1
double n = n*2
square n = n^2

Let’s see how to re-create the previous definitions by using the power of first-class functions:

ifEvenInc n = ifEven inc n
ifEvenDouble n = ifEven double n
ifEvenSquare n = ifEven square n

Now you can easily handle adding new functions such as ifEvenCube or ifEvenNegate.

Function and operator precedence

In this lesson, you’ve already seen examples of functions and operators. For example, inc is a function and + is an operator. An important part of writing Haskell code is that functions are always evaluated before operators. What does this mean? Take this example in GHCi:

GHCi> 1 + 2 * 3
7

As in most programming languages, * has a higher precedence than +, so you multiply 2 and 3 and then add 1, giving you 7. Now let’s look what happens when you replace 1 + with inc:

GHCi> inc 2 * 3
9

This result is different because functions always have precedence over operators. This means that inc 2 is evaluated first and then the result is multiplied by 3. This is true even for multi-argument functions:

GHCi> add x y = x + y
GHCi> add 1 2 * 3
9

The key benefit is that this enables you to avoid using a large number of unnecessary parentheses in your code.

4.1.1. Lambda functions as arguments

Naming functions is generally a good idea, but you can also use lambda functions to quickly add code to pass into a function. If you want to double the value, you can quickly put together a lambda function for this:

GHCi> ifEven (x -> x*2) 6
12

Although named functions are preferred, many times you’ll want to pass in simple functionality.

Quick check 4.1

Q1:

Write a lambda function for cubing x and pass it to ifEven.

QC 4.1 answer

1:

GHCi> ifEven (x -> x^3) 4

 

4.1.2. Example—custom sorting

A practical use of passing functions into other functions is for sorting. Suppose you have a list of first and last names. In this example, each name is represented as a tuple. A tuple is a type that’s like a list, but it can contain multiple types and is of a fixed size. Here’s an example of a name in a tuple:

author = ("Will","Kurt")

Tuples of two items (a pair) have two useful functions, fst and snd, which access the first and second elements of the tuple, respectively:

GHCi> fst author
"Will"
GHCi> snd author
"Kurt"

Now suppose you have a list of names you want to sort. Here’s a set of names represented as a list of tuples.

Listing 4.4. names
names = [("Ian", "Curtis"),
         ("Bernard","Sumner"),
         ("Peter", "Hook"),
         ("Stephen","Morris")]

You want to sort names. Thankfully, Haskell does have a built-in sort function. To use it, you first need to import the Data.List module. To do this is fairly straightforward; you need to add the following declaration to the top of whatever file you’re working in:

import Data.List

Alternatively, you can import into GHCi. If you load a file with names and your import, you can see that Haskell’s sort takes a good guess at how to sort these tuples:

GHCi> sort names
[("Bernard","Sumner"),("Ian", "Curtis"),("Peter", "Hook"),
("Stephen","Morris")]

Not bad, given Haskell has no idea what you’re trying to do! Unfortunately, you usually don’t want to sort by first name. To solve this, you can use Haskell’s sortBy function, which is included in the Data.List module. You need to supply sortBy with another function that will compare two of your tuple names. After you explain how to compare two elements, the rest is taken care of. For this, you write a function compareLastNames. This function takes two arguments, name1 and name2, and returns GT, LT, or EQ. GT, LT, and EQ are special values representing greater than, less than, and equal. In many programming languages, you’d return True or False, or 1, -1, or 0.

Listing 4.5. compareLastNames
compareLastNames name1 name2 = if lastName1 > lastName2
                               then GT
                               else if lastName1 < lastName2
                                    then LT
                                    else EQ
  where lastName1 = snd name1
        lastName2 = snd name2

Now you can go back to GHCi and use sortBy with your custom sorting:

GHCi> sortBy compareLastNames names
[("Ian", "Curtis"),("Peter", "Hook"),("Stephen","Morris),
("Bernard","Sumner")]

Much better! JavaScript, Ruby, and Python all support a similar use of first-class functions for custom sorting, so this technique is likely familiar to many programmers.

Quick check 4.2

Q1:

In compareLastNames, you didn’t handle the case of having two last names that are the same but with different first names. Modify the compareLastNamesfunction to compare first names and use it to fix compareLastNames.

QC 4.2 answer

1:

compareLastNames name1 name2 = if lastName1 > lastName2
                               then GT
                               else if lastName1 < lastName2
                                    then LT
                                    else if firstName1 > firstName2
                                         then GT
                                         else if firstName1 < firstName2
                                              then LT
                                              else EQ
where lastName1 = snd name1
      lastName2 = snd name2
      firstName1 = fst name1
      firstName2 = fst name2

 

4.2. Returning functions

We’ve talked a fair bit about passing functions as arguments, but this is only half of what it means to have first-class functions as values. Functions also return values, so for truly first-class functions, it makes sense that functions must sometimes return other functions. As always, the question should be, why would I ever want to return a function? One good reason is that you want to dispatch certain functions based on other parameters.

Suppose you create a Secret Society of Contemporary Alchemists and you need to send newsletters to members at various regional post office boxes. There are offices in three cities: San Francisco, Reno, and New York. Here are the office addresses:

  • PO Box 1234, San Francisco, CA, 94111
  • PO Box 789, New York, NY, 10013
  • PO Box 456, Reno, NV, 89523

You need to build a function that will take a name tuple (as you used before in the sorting example) and an office location and then put together the mailing address for you. A first pass at this function might look like the following. The only other thing we need to introduce that you haven’t seen yet is the ++ operator used to concatenate strings (and lists).

Listing 4.6. addressLetter v.1
addressLetter name location = nameText ++ " - " ++location
  where nameText = (fst name) ++ " " ++ (snd name)

To use this function, you have to pass a name tuple and the full address:

GHCi> addressLetter ("Bob","Smith") "PO Box 1234 - San Francisco, CA, 94111"
"Bob Smith - PO Box 1234 - San Francisco, CA, 94111"

This is a fine solution. You also could easily use variables to keep track of the addresses, and that would make errors much less likely (and save typing). You’re all set to send out your newsletters!

After the first round of newsletters, you get some complaints and requests from the regional offices:

  • San Francisco added a new address for members with last names starting with the letter L or later in the alphabet: PO Box 1010, San Francisco, CA, 94109.
  • New York wants the name followed by a colon rather than a hyphen, for mystical reasons they won’t share.
  • Reno wants only last names to be used for greater secrecy.

It’s clear that now you need a different function for each office.

Listing 4.7. sfOffice, nyOffice, renoOffice
sfOffice name = if lastName < "L"
                then nameText
                     ++ " - PO Box 1234 - San Francisco, CA, 94111"
                else nameText
                     ++ " - PO Box 1010 - San Francisco, CA, 94109"
  where lastName = snd name
        nameText = (fst name) ++ " " ++ lastName

nyOffice name = nameText ++ ": PO Box 789 - New York, NY, 10013"
  where nameText = (fst name) ++ " " ++ (snd name)
renoOffice name = nameText ++ " - PO Box 456 - Reno, NV 89523"
  where nameText = snd name

The question now is, how should you use these three functions with addressLetter? You could rewrite addressLetter to take a function rather than a location as an argument. The trouble with this is that the addressLetter function is going to be part of a larger web application, and you’d like to pass in a string parameter for the location. What you’d really like is another function that will take a location string and dispatch the right function for you. You’ll build a new function called getLocationFunction that will take a single string and dispatch the correct function. Rather than a bunch of nested if then else expressions, you’ll use Haskell’s case expression.

Listing 4.8. getLocationFunction
getLocationFunction location = case location of          1
  "ny" -> nyOffice                                       2
  "sf" -> sfOffice                                       3
  "reno" -> renoOffice                                   4
  _ -> (
ame -> (fst name) ++ " " ++ (snd name))        5

  • 1 case looks at the value of location.
  • 2 If location is ny, returns nyOffice
  • 3 If location is sf, returns sfOffice
  • 4 If location is reno, returns renoOffice
  • 5 If it’s anything else (_ is a wildcard), returns a generic solution

This case expression should seem straightforward, except for that underscore (_) at the end. You want to capture the situation in which a string other than one of the official locations is passed in. In Haskell, _ is used frequently as a wildcard. This is covered in much more depth in the next lesson. In this case, if the user of your code passes in an invalid location, you put together a quick lambda function that will make the name tuple into a string. Now you have a single function that will return the function you need when you need it. Finally, you can rewrite addressLetter, as shown next.

Listing 4.9. addressLetter v.2
addressLetter name location = locationFunction name
  where locationFunction = getLocationFunction location

In GHCi, you can test that your function performs as expected:

GHCi> addressLetter ("Bob","Smith") "ny"
"Bob Smith: PO Box 789 - New York, NY, 10013"

GHCi> addressLetter ("Bob","Jones") "ny"
"Bob Jones: PO Box 789 - New York, NY, 10013"

GHCi> addressLetter ("Samantha","Smith") "sf"
"Samantha Smith - PO Box 1010 - San Francisco, CA, 94109"

GHCi> addressLetter ("Bob","Smith") "reno"
"Smith - PO Box 456 - Reno, NV 89523"

GHCi> addressLetter ("Bob","Smith") "la"
"Bob Smith"

Now that you’ve separated each function needed for generating addresses, you can easily add new rules as they come in from each office. In this example, returning functions as values helped tremendously to make your code easier to understand and extend. This is a simple use of returning functions as values; all you’ve done is automate the way functions can move around.

Summary

In this lesson, our objective was to explain first-class functions. First-class functions allow you to pass functions in as arguments as well as return them as values. First-class functions are an incredibly powerful tool, because they allow you to abstract out computation from your functions. The power of first-class functions is evidenced by their wide adoption in most modern programming languages. Let’s see if you got this.

Q4.1

Anything that can be compared in Haskell (for example, [Char], which you use for the names in your name tuples) can be compared with a function called compare. The compare function returns GT, LT, or EQ. Rewrite compareLastNames by using compare.

Q4.2

Define a new location function for Washington, DC and add it to getLocation-Function. In the DC function, everyone’s names must be followed by Esq.

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

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