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

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

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

Добавлен: 02.01.2026

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

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

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

Section 6.9

Chapter 6 · Functional Objects

149

a variable, for example, the “n / g” that initializes numer.) Because g is private, it can be accessed inside the body of the class, but not outside. We also added a private method, gcd, which calculates the greatest common divisor of two passed Ints. For example, gcd(12, 8) is 4. As you saw in Section 4.1, to make a field or method private you simply place the private keyword in front of its definition. The purpose of the private “helper method” gcd is to factor out code needed by some other part of the class, in this case, the primary constructor. To ensure g is always positive, we pass the absolute value of n and d, which we obtain by invoking abs on them, a method you can invoke on any Int to get its absolute value.

The Scala compiler will place the code for the initializers of Rational’s three fields into the primary constructor in the order in which they appear in the source code. Thus, g’s initializer, gcd(n.abs, d.abs), will execute before the other two, because it appears first in the source. Field g will be initialized with the result, the greatest common divisor of the absolute value of the class parameters, n and d. Field g is then used in the initializers of numer and denom. By dividing n and d by their greatest common divisor, g, every Rational will be constructed in its normalized form:

scala> new Rational(66, 42) res7: Rational = 11/7

6.9Defining operators

The current implementation of Rational addition is OK, but could be made more convenient to use. You might ask yourself why you can write:

x + y

if x and y are integers or floating-point numbers, but you have to write:

x.add(y)

or at least:

x add y

if they are rational numbers. There’s no convincing reason why this should be so. Rational numbers are numbers just like other numbers. In a mathematical sense they are even more natural than, say, floating-point numbers.

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

Section 6.9

Chapter 6 · Functional Objects

150

Why should you not use the natural arithmetic operators on them? In Scala you can do this. In the rest of this chapter, we’ll show you how.

The first step is to replace add by the usual mathematical symbol. This is straightforward, as + is a legal identifier in Scala. We can simply define a method with + as its name. While we’re at it, you may as well implement a method named * that performs multiplication. The result is shown in Listing 6.4:

class Rational(n: Int, d: Int) {

require(d != 0)

private val g = gcd(n.abs, d.abs) val numer = n / g

val denom = d / g

def this(n: Int) = this(n, 1)

def + (that: Rational): Rational = new Rational(

numer * that.denom + that.numer * denom, denom * that.denom

)

def * (that: Rational): Rational =

new Rational(numer * that.numer, denom * that.denom) override def toString = numer +"/"+ denom

private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

}

Listing 6.4 · Rational with operator methods.

With class Rational defined in this manner, you can now write:

scala> val x = new Rational(1, 2) x: Rational = 1/2

scala> val y = new Rational(2, 3) y: Rational = 2/3

scala> x + y

res8: Rational = 7/6

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


Section 6.10

Chapter 6 · Functional Objects

151

As always, the operator syntax on the last input line is equivalent to a method call. You could also write:

scala> x.+(y)

res9: Rational = 7/6

but this is not as readable.

Another thing to note is that given Scala’s rules for operator precedence, which were described in Section 5.8, the * method will bind more tightly than the + method for Rationals. In other words, expressions involving + and * operations on Rationals will behave as expected. For example, x + x * y will execute as x + (x * y), not (x + x) * y:

scala> x + x * y res10: Rational = 5/6

scala> (x + x) * y res11: Rational = 2/3

scala> x + (x * y) res12: Rational = 5/6

6.10 Identifiers in Scala

You have now seen the two most important ways to form an identifier in Scala: alphanumeric and operator. Scala has very flexible rules for forming identifiers. Besides the two forms you have seen there are also two others. All four forms of identifier formation are described in this section.

An alphanumeric identifier starts with a letter or underscore, which can be followed by further letters, digits, or underscores. The ‘$’ character also counts as a letter, however it is reserved for identifiers generated by the Scala compiler. Identifiers in user programs should not contain ‘$’ characters, even though it will compile; if they do this might lead to name clashes with identifiers generated by the Scala compiler.

Scala follows Java’s convention of using camel-case5 identifiers, such as toString and HashSet. Although underscores are legal in identifiers, they are not used that often in Scala programs, in part to be consistent with Java,

5This style of naming identifiers is called camel case because the identifiersHaveHumps consisting of the embedded capital letters.

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

Section 6.10

Chapter 6 · Functional Objects

152

but also because underscores have many other non-identifier uses in Scala code. As a result, it is best to avoid identifiers like to_string, __init__, or name_. Camel-case names of fields, method parameters, local variables, and functions should start with lower case letter, for example: length, flatMap, and s. Camel-case names of classes and traits should start with an upper case letter, for example: BigInt, List, and UnbalancedTreeMap.6

Note

One consequence of using a trailing underscore in an identifier is that if you attempt, for example, to write a declaration like this,

val name_: Int = 1”, you’ll get a compiler error. The compiler will think you are trying to declare a val named “name_:”. To get this to compile, you would need to insert an extra space before the colon, as in: “val name_ : Int = 1”.

One way in which Scala’s conventions depart from Java’s involves constant names. In Scala, the word constant does not just mean val. Even though a val does remain constant after it is initialized, it is still a variable. For example, method parameters are vals, but each time the method is called those vals can hold different values. A constant is more permanent. For example, scala.math.Pi is defined to be the double value closest to the real value of p, the ratio of a circle’s circumference to its diameter. This value is unlikely to change ever, thus, Pi is clearly a constant. You can also use constants to give names to values that would otherwise be magic numbers in your code: literal values with no explanation, which in the worst case appear in multiple places. You may also want to define constants for use in pattern matching, a use case that will be described in Section 15.2. In Java, the convention is to give constants names that are all upper case, with underscores separating the words, such as MAX_VALUE or PI. In Scala, the convention is merely that the first character should be upper case. Thus, constants named in the Java style, such as X_OFFSET, will work as Scala constants, but the Scala convention is to use camel case for constants, such as XOffset.

An operator identifier consists of one or more operator characters. Operator characters are printable ASCII characters such as +, :, ?, ~ or #.7 Here

6In Section 16.5, you’ll see that sometimes you may want to give a special kind of class known as a case class a name consisting solely of operator characters. For example, the Scala API contains a class named ::, which facilitates pattern matching on Lists.

7More precisely, an operator character belongs to the Unicode set of mathematical symbols(Sm) or other symbols(So), or to the 7-bit ASCII characters that are not letters, digits,

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


Section 6.10

Chapter 6 · Functional Objects

153

are some examples of operator identifiers:

+++ ::: <?> :->

The Scala compiler will internally “mangle” operator identifiers to turn them into legal Java identifiers with embedded $ characters. For instance, the identifier :-> would be represented internally as $colon$minus$greater. If you ever wanted to access this identifier from Java code, you’d need to use this internal representation.

Because operator identifiers in Scala can become arbitrarily long, there is a small difference between Java and Scala. In Java, the input x<-y would be parsed as four lexical symbols, so it would be equivalent to x < - y. In Scala, <- would be parsed as a single identifier, giving x <- y. If you want the first interpretation, you need to separate the < and the - characters by a space. This is unlikely to be a problem in practice, as very few people would write x<-y in Java without inserting spaces or parentheses between the operators.

A mixed identifier consists of an alphanumeric identifier, which is followed by an underscore and an operator identifier. For example, unary_+ used as a method name defines a unary + operator. Or, myvar_= used as method name defines an assignment operator. In addition, the mixed identifier form myvar_= is generated by the Scala compiler to support properties; more on that in Chapter 18.

A literal identifier is an arbitrary string enclosed in back ticks (` . . . `). Some examples of literal identifiers are:

`x` `<clinit>` `yield`

The idea is that you can put any string that’s accepted by the runtime as an identifier between back ticks. The result is always a Scala identifier. This works even if the name contained in the back ticks would be a Scala reserved word. A typical use case is accessing the static yield method in Java’s Thread class. You cannot write Thread.yield() because yield is a reserved word in Scala. However, you can still name the method in back ticks, e.g., Thread.`yield`().

parentheses, square brackets, curly braces, single or double quote, or an underscore, period, semi-colon, comma, or back tick character.

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


Section 6.11

Chapter 6 · Functional Objects

154

6.11 Method overloading

Back to class Rational. With the latest changes, you can now do addition and multiplication operations in a natural style on rational numbers. But one thing still missing is mixed arithmetic. For instance, you cannot multiply a rational number by an integer, because the operands of * always have to be Rationals. So for a rational number r you can’t write r * 2. You must write r * new Rational(2), which is not as nice.

To make Rational even more convenient, we’ll add new methods to the class that perform mixed addition and multiplication on rational numbers and integers. While we’re at it, we’ll add methods for subtraction and division too. The result is shown in Listing 6.5.

There are now two versions each of the arithmetic methods: one that takes a rational as its argument and another that takes an integer. In other words, each of these method names is overloaded, because each name is now being used by multiple methods. For example, the name + is used by one method that takes a Rational and another that takes an Int. In a method call, the compiler picks the version of an overloaded method that correctly matches the types of the arguments. For instance, if the argument y in x.+(y) is a Rational, the compiler will pick the method + that takes a Rational parameter. But if the argument is an integer, the compiler will pick the method + that takes an Int parameter instead. If you try this:

scala> val x = new Rational(2, 3) x: Rational = 2/3

scala> x * x

res13: Rational = 4/9

scala> x * 2

res14: Rational = 4/3

You’ll see that the * method invoked is determined in each case by the type of the right operand.

Note

Scala’s process of overloaded method resolution is very similar to Java’s. In every case, the chosen overloaded version is the one that best matches the static types of the arguments. Sometimes there is no unique best matching version; in that case the compiler will give you an “ambiguous reference” error.

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

Section 6.11

Chapter 6 · Functional Objects

155

class Rational(n: Int, d: Int) {

require(d != 0)

private val g = gcd(n.abs, d.abs) val numer = n / g

val denom = d / g

def this(n: Int) = this(n, 1)

def + (that: Rational): Rational = new Rational(

numer * that.denom + that.numer * denom, denom * that.denom

)

def + (i: Int): Rational =

new Rational(numer + i * denom, denom)

def - (that: Rational): Rational = new Rational(

numer * that.denom - that.numer * denom, denom * that.denom

)

def - (i: Int): Rational =

new Rational(numer - i * denom, denom)

def * (that: Rational): Rational =

new Rational(numer * that.numer, denom * that.denom)

def * (i: Int): Rational =

new Rational(numer * i, denom)

def / (that: Rational): Rational =

new Rational(numer * that.denom, denom * that.numer)

def / (i: Int): Rational =

new Rational(numer, denom * i)

override def toString = numer +"/"+ denom

private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

}

Listing 6.5 · Rational with overloaded methods.

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


Section 6.12

Chapter 6 · Functional Objects

156

6.12 Implicit conversions

Now that you can write r * 2, you might also want to swap the operands, as in 2 * r. Unfortunately this does not work yet:

scala> 2 * r

<console>:7: error: overloaded method value * with alternatives (Double)Double <and> (Float)Float <and> (Long)Long <and> (Int)Int <and> (Char)Int <and> (Short)Int <and> (Byte)Int cannot be applied to (Rational)

2 * r

ˆ

The problem here is that 2 * r is equivalent to 2.*(r), so it is a method call on the number 2, which is an integer. But the Int class contains no multiplication method that takes a Rational argument—it couldn’t because class Rational is not a standard class in the Scala library.

However, there is another way to solve this problem in Scala: You can create an implicit conversion that automatically converts integers to rational numbers when needed. Try adding this line in the interpreter:

scala> implicit def intToRational(x: Int) = new Rational(x)

This defines a conversion method from Int to Rational. The implicit modifier in front of the method tells the compiler to apply it automatically in a number of situations. With the conversion defined, you can now retry the example that failed before:

scala> val r = new Rational(2,3) r: Rational = 2/3

scala> 2 * r

res16: Rational = 4/3

Note that for an implicit conversion to work, it needs to be in scope. If you place the implicit method definition inside class Rational, it won’t be in scope in the interpreter. For now, you’ll need to define it directly in the interpreter.

As you can glimpse from this example, implicit conversions are a very powerful technique for making libraries more flexible and more convenient to use. Because they are so powerful, they can also be easily misused. You’ll

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

Section 6.13

Chapter 6 · Functional Objects

157

find out more on implicit conversions, including ways to bring them into scope where they are needed, in Chapter 21.

6.13 A word of caution

As this chapter has demonstrated, creating methods with operator names and defining implicit conversions can help you design libraries for which client code is concise and easy to understand. Scala gives you a great deal of power to design such easy-to-use libraries, but please bear in mind that with power comes responsibility.

If used unartfully, both operator methods and implicit conversions can give rise to client code that is hard to read and understand. Because implicit conversions are applied implicitly by the compiler, not explicitly written down in the source code, it can be non-obvious to client programmers what implicit conversions are being applied. And although operator methods will usually make client code more concise, they will only make it more readable to the extent client programmers will be able to recognize and remember the meaning of each operator.

The goal you should keep in mind as you design libraries is not merely enabling concise client code, but readable, understandable client code. Conciseness will often be a big part of that readability, but you can take conciseness too far. By designing libraries that enable tastefully concise and at the same time understandable client code, you can help those client programmers work productively.

6.14 Conclusion

In this chapter, you saw more aspects of classes in Scala. You saw how to add parameters to a class, define several constructors, define operators as methods, and customize classes so that they are natural to use. Maybe most importantly, you saw that defining and using immutable objects is a quite natural way to code in Scala.

Although the final version of Rational shown in this chapter fulfills the requirements set forth at the beginning of the chapter, it could still be improved. We will in fact return to this example later in the book. For example, in Chapter 30, you’ll learn how to override equals and hashcode to allow Rationals to behave better when compared with == or placed into hash ta-

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