ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 02.01.2026

Просмотров: 3451

Скачиваний: 0

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Section 9.1

Chapter 9 · Control Abstraction

209

Experienced programmers will notice all of this repetition and wonder if it can be factored into a common helper function. Doing it the obvious way does not work, however. You would like to be able to do the following:

def filesMatching(query: String, method) =

for (file <- filesHere; if file.getName.method(query)) yield file

This approach would work in some dynamic languages, but Scala does not allow pasting together code at runtime like this. So what do you do?

Function values provide an answer. While you cannot pass around a method name as a value, you can get the same effect by passing around a function value that calls the method for you. In this case, you could add a matcher parameter to the method whose sole purpose is to check a file name against a query:

def filesMatching(query: String,

matcher: (String, String) => Boolean) = {

for (file <- filesHere; if matcher(file.getName, query)) yield file

}

In this version of the method, the if clause now uses matcher to check the file name against the query. Precisely what this check does depends on what is specified as the matcher. Take a look, now, at the type of matcher itself. It is a function, and thus has a => in the type. This function takes two string arguments—the file name and the query—and returns a boolean, so the type of this function is (String, String) => Boolean.

Given this new filesMatching helper method, you can simplify the three searching methods by having them call the helper method, passing in an appropriate function:

def filesEnding(query: String) = filesMatching(query, _.endsWith(_))

def filesContaining(query: String) = filesMatching(query, _.contains(_))

def filesRegex(query: String) = filesMatching(query, _.matches(_))

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 9.1

Chapter 9 · Control Abstraction

210

The function literals shown in this example use the placeholder syntax, introduced in the previous chapter, which may not as yet feel very natural to you. Thus, here’s a clarification of how placeholders are used in this example. The function literal _.endsWith(_), used in the filesEnding method, means the same thing as:

(fileName: String, query: String) => fileName.endsWith(query)

Because filesMatching takes a function that requires two String arguments, however, you need not specify the types of the arguments. Thus you could also write (fileName, query) => fileName.endsWith(query). Since the parameters are each used only once in the body of the function, and since the first parameter, fileName, is used first in the body, and the second parameter, query, is used second, you can use the placeholder syntax: _.endsWith(_). The first underscore is a placeholder for the first parameter, the file name, and the second underscore a placeholder for the second parameter, the query string.

This code is already simplified, but it can actually be even shorter. Notice that the query gets passed to filesMatching, but filesMatching does nothing with the query except to pass it back to the passed matcher function. This passing back and forth is unnecessary, because the caller already knew the query to begin with! You might as well simply remove the query parameter from filesMatching and matcher, thus simplifying the code as shown in Listing 9.1.

This example demonstrates the way in which first-class functions can help you eliminate code duplication where it would be very difficult to do so without them. In Java, for example, you could create an interface containing a method that takes one String and returns a Boolean, then create and pass anonymous inner class instances that implement this interface to filesMatching. Although this approach would remove the code duplication you are trying to eliminate, it would at the same time add as much or more new code. Thus the benefit is not worth the cost, and you may as well live with the duplication.

Moreover, this example demonstrates how closures can help you reduce code duplication. The function literals used in the previous example, such as _.endsWith(_) and _.contains(_), are instantiated at runtime into function values that are not closures, because they don’t capture any free variables. Both variables used in the expression, _.endsWith(_), for example, are represented by underscores, which means they are taken from arguments

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index



Section 9.2

Chapter 9 · Control Abstraction

211

object FileMatcher {

private def filesHere = (new java.io.File(".")).listFiles

private def filesMatching(matcher: String => Boolean) = for (file <- filesHere; if matcher(file.getName))

yield file

def filesEnding(query: String) = filesMatching(_.endsWith(query))

def filesContaining(query: String) = filesMatching(_.contains(query))

def filesRegex(query: String) = filesMatching(_.matches(query))

}

Listing 9.1 · Using closures to reduce code duplication.

to the function. Thus, _.endsWith(_) uses two bound variables, and no free variables. By contrast, the function literal _.endsWith(query), used in the most recent example, contains one bound variable, the argument represented by the underscore, and one free variable named query. It is only because Scala supports closures that you were able to remove the query parameter from filesMatching in the most recent example, thereby simplifying the code even further.

9.2Simplifying client code

The previous example demonstrated that higher-order functions can help reduce code duplication as you implement an API. Another important use of higher-order functions is to put them in an API itself to make client code more concise. A good example is provided by the special-purpose looping methods of Scala’s collection types.1 Many of these are listed in Table 3.1 in Chapter 3, but take a look at just one example for now to see why these methods are so useful.

1These special-purpose looping methods are defined in trait Traversable, which is extended by List, Set, and Map. See Chapter 17 for a discussion.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 9.2

Chapter 9 · Control Abstraction

212

Consider exists, a method that determines whether a passed value is contained in a collection. You could of course search for an element by having a var initialized to false, looping through the collection checking each item, and setting the var to true if you find what you are looking for. Here’s a method that uses this approach to determine whether a passed List contains a negative number:

def containsNeg(nums: List[Int]): Boolean = { var exists = false

for (num <- nums) if (num < 0)

exists = true exists

}

If you define this method in the interpreter, you can call it like this:

scala> containsNeg(List(1, 2, 3, 4)) res0: Boolean = false

scala> containsNeg(List(1, 2, -3, 4)) res1: Boolean = true

A more concise way to define the method, though, is by calling the higherorder function exists on the passed List, like this:

def containsNeg(nums: List[Int]) = nums.exists(_ < 0)

This version of containsNeg yields the same results as the previous:

scala> containsNeg(Nil) res2: Boolean = false

scala> containsNeg(List(0, -1, -2)) res3: Boolean = true

The exists method represents a control abstraction. It is a special-purpose looping construct provided by the Scala library rather than being built into the Scala language like while or for. In the previous section, the higherorder function, filesMatching, reduces code duplication in the implementation of the object FileMatcher. The exists method provides a similar benefit, but because exists is public in Scala’s collections API, the code

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 9.3

Chapter 9 · Control Abstraction

213

duplication it reduces is client code of that API. If exists didn’t exist, and you wanted to write a containsOdd method, to test whether a list contains odd numbers, you might write it like this:

def containsOdd(nums: List[Int]): Boolean = { var exists = false

for (num <- nums) if (num % 2 == 1)

exists = true exists

}

If you compare the body of containsNeg with that of containsOdd, you’ll find that everything is repeated except the test condition of an if expression. Using exists, you could write this instead:

def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)

The body of the code in this version is again identical to the body of the corresponding containsNeg method (the version that uses exists), except the condition for which to search is different. Yet the amount of code duplication is much smaller because all of the looping infrastructure is factored out into the exists method itself.

There are many other looping methods in Scala’s standard library. As with exists, they can often shorten your code if you recognize opportunities to use them.

9.3Currying

In Chapter 1, we said that Scala allows you to create new control abstractions that “feel like native language support.” Although the examples you’ve seen so far are indeed control abstractions, it is unlikely anyone would mistake them for native language support. To understand how to make control abstractions that feel more like language extensions, you first need to understand the functional programming technique called currying.

A curried function is applied to multiple argument lists, instead of just one. Listing 9.2 shows a regular, non-curried function, which adds two Int parameters, x and y.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 9.3

Chapter 9 · Control Abstraction

214

scala> def plainOldSum(x: Int, y: Int) = x + y plainOldSum: (x: Int,y: Int)Int

scala> plainOldSum(1, 2) res4: Int = 3

Listing 9.2 · Defining and invoking a “plain old” function.

By contrast, Listing 9.3 shows a similar function that’s curried. Instead of one list of two Int parameters, you apply this function to two lists of one Int parameter each.

scala> def curriedSum(x: Int)(y: Int) = x + y curriedSum: (x: Int)(y: Int)Int

scala> curriedSum(1)(2) res5: Int = 3

Listing 9.3 · Defining and invoking a curried function.

What’s happening here is that when you invoke curriedSum, you actually get two traditional function invocations back to back. The first function invocation takes a single Int parameter named x, and returns a function value for the second function. This second function takes the Int parameter y. Here’s a function named first that does in spirit what the first traditional function invocation of curriedSum would do:

scala> def first(x: Int) = (y: Int) => x + y first: (x: Int)(Int) => Int

Applying 1 to the first function—in other words, invoking the first function and passing in 1—yields the second function:

scala> val second = first(1) second: (Int) => Int = <function1>

Applying 2 to the second function yields the result:

scala> second(2) res6: Int = 3

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index