Назад
Section 20.9 Chapter 20 · Abstract Members 461
object Converter {
var exchangeRate = Map(
"USD" -> Map("USD" -> 1.0 , "EUR" -> 0.7596,
"JPY" -> 1.211 , "CHF" -> 1.223),
"EUR" -> Map("USD" -> 1.316 , "EUR" -> 1.0 ,
"JPY" -> 1.594 , "CHF" -> 1.623),
"JPY" -> Map("USD" -> 0.8257, "EUR" -> 0.6272,
"JPY" -> 1.0 , "CHF" -> 1.018),
"CHF" -> Map("USD" -> 0.8108, "EUR" -> 0.6160,
"JPY" -> 0.982 , "CHF" -> 1.0 )
)
}
Listing 20.13 · A converter object with an exchange rates map.
Then, you could add a conversion method, from, to class Currency, which
converts from a given source currency into the current Currency object:
def from(other: CurrencyZone#AbstractCurrency): Currency =
make(Math.round(
other.amount.toDouble
*
Converter.exchangeRate
(other.designation)(this.designation)))
The from method takes an arbitrary currency as argument. This is expressed
by its formal parameter type, CurrencyZone#AbstractCurrency, which
indicates that the argument passed as other must be an AbstractCurrency
type in some arbitrary and unknown CurrencyZone. It produces its result
by multiplying the amount of the other currency with the exchange rate
between the other and the current currency.
3
The final version of the CurrencyZone class is shown in Listing 20.14.
You can test the class in the Scala command shell. We’ll assume that the
CurrencyZone class and all concrete CurrencyZone objects are defined in
a package org.stairwaybook.currencies. The first step is to import ev-
erything in this package into the command shell:
scala> import org.stairwaybook.currencies._
3
By the way, in case you think you’re getting a bad deal on Japanese yen, the exchange
rates convert currencies based on their CurrencyZone amounts. Thus, 1.211 is the exchange
rate between US cents to Japanese yen.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 20.9 Chapter 20 · Abstract Members 462
abstract class CurrencyZone {
type Currency <: AbstractCurrency
def make(x: Long): Currency
abstract class AbstractCurrency {
val amount: Long
def designation: String
def + (that: Currency): Currency =
make(this.amount + that.amount)
def
*
(x: Double): Currency =
make((this.amount
*
x).toLong)
def - (that: Currency): Currency =
make(this.amount - that.amount)
def / (that: Double) =
make((this.amount / that).toLong)
def / (that: Currency) =
this.amount.toDouble / that.amount
def from(other: CurrencyZone#AbstractCurrency): Currency =
make(Math.round(
other.amount.toDouble
*
Converter.exchangeRate
(other.designation)(this.designation)))
private def decimals(n: Long): Int =
if (n == 1) 0 else 1 + decimals(n / 10)
override def toString =
((amount.toDouble / CurrencyUnit.amount.toDouble)
formatted ("%."+ decimals(CurrencyUnit.amount) +"f")
+" "+ designation)
}
val CurrencyUnit: Currency
}
Listing 20.14 · The full code of class CurrencyZone.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 20.10 Chapter 20 · Abstract Members 463
You can then do some currency conversions:
scala> Japan.Yen from US.Dollar
*
100
res16: Japan.Currency = 12110 JPY
scala> Europe.Euro from res16
res17: Europe.Currency = 75.95 EUR
scala> US.Dollar from res17
res18: US.Currency = 99.95 USD
The fact that we obtain almost the same amount after three conversions im-
plies that these are some pretty good exchange rates!
You can also add up values of the same currency:
scala> US.Dollar
*
100 + res18
res19: currencies.US.Currency = 199.95
On the other hand, you cannot add amounts of different currencies:
scala> US.Dollar + Europe.Euro
<console>:7: error: type mismatch;
found : currencies.Europe.Euro
required: currencies.US.Currency
US.Dollar + Europe.Euro
ˆ
By preventing the addition of two values with different units (in this case,
currencies), the type abstraction has done its job. It prevents us from per-
forming calculations that are unsound. Failures to convert correctly between
different units may seem like trivial bugs, but they have caused many seri-
ous systems faults. An example is the crash of the Mars Climate Orbiter
spacecraft on September 23, 1999, which was caused because one engineer-
ing team used metric units while another used English units. If units had
been coded in the same way as currencies are coded in this chapter, this error
would have been detected by a simple compilation run. Instead, it caused the
crash of the orbiter after a near ten-month voyage.
20.10 Conclusion
Scala offers systematic and very general support for object-oriented abstrac-
tion. It enables you to not only abstract over methods, but also over values,
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 20.10 Chapter 20 · Abstract Members 464
variables, and types. This chapter has shown how to take advantage of ab-
stract members. They support a simple yet effective principle for systems
structuring: when designing a class, make everything that is not yet known
into an abstract member. The type system will then drive the development of
your model, just as you saw with the currency case study. It does not matter
whether the unknown is a type, method, variable or value. In Scala, all of
these can be declared abstract.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Chapter 21
Implicit Conversions and Parameters
There’s a fundamental difference between your own code and libraries of
other people: you can change or extend your own code as you wish, but if
you want to use someone else’s libraries, you usually have to take them as
they are.
A number of constructs have sprung up in programming languages to
alleviate this problem. Ruby has modules, and Smalltalk lets packages add
to each other’s classes. These are very powerful, but also dangerous, in that
you modify the behavior of a class for an entire application, some parts of
which you might not know. C# 3.0 has static extension methods, which are
more local, but also more restrictive in that you can only add methods, not
fields, to a class, and you can’t make a class implement new interfaces.
Scala’s answer is implicit conversions and parameters. These can make
existing libraries much more pleasant to deal with by letting you leave out
tedious, obvious details that obscure the interesting parts of your code. Used
tastefully, this results in code that is focused on the interesting, non-trivial
parts of your program. This chapter shows you how implicits work, and
presents some of the most common ways they are used.
21.1 Implicit conversions
Before delving into the details of implicit conversions, take a look at a typ-
ical example of their use. One of the central collection traits in Scala is
RandomAccessSeq[T], which describes random access sequences over ele-
ments of type T. RandomAccessSeqs have most of the utility methods that
you know from arrays or lists: take, drop, map, filter, exists, and
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 21.1 Chapter 21 · Implicit Conversions and Parameters 466
mkString are just some examples. To make a new random access sequence,
all you must do is extend trait RandomAccessSeq. You only need to define
two methods that are abstract in the trait: length and apply. You then get
implementations of all the other useful methods in the trait “for free.
So far so good. This works fine if you are about to define new classes,
but what about existing ones? Maybe you’d like to also treat classes in other
people’s libraries as random access sequences, even if the designers of those
libraries had not thought of making their classes extend RandomAccessSeq.
For instance, a String in Java would make a fine RandomAccessSeq[Char],
except that unfortunately Java’s String class does not inherit from Scala’s
RandomAccessSeq trait.
In situations like this, implicits can help. To make a String appear to be
a subtype of RandomAccessSeq, you can define an implicit conversion from
String to an adapter class that actually is a subtype of RandomAccessSeq:
implicit def stringWrapper(s: String) =
new RandomAccessSeq[Char] {
def length = s.length
def apply(i: Int) = s.charAt(i)
}
That’s it.
1
The implicit conversion is just a normal method. The only thing
that’s special is the implicit modifier at the start. You can apply the con-
version explicitly to transform Strings to RandomAccessSeqs:
scala> stringWrapper("abc123") exists (_.isDigit)
res0: Boolean = true
But you can also leave out the conversion and still get the same behavior:
scala> "abc123" exists (_.isDigit)
res1: Boolean = true
What goes on here under the covers is that the Scala compiler inserts the
stringWrapper conversion for you. So in effect it rewrites the last expres-
sion above to the one before. But on the surface, it’s as if Java’s Strings had
acquired all the useful methods of trait RandomAccessSeq.
1
In fact, the Predef object already defines a stringWrapper conversion with similar
functionality, so in practice you can use this conversion instead of defining your own.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 21.1 Chapter 21 · Implicit Conversions and Parameters 467
This aspect of implicits is similar to extension methods in C#, which also
allow you to add new methods to existing classes. However, implicits can
be far more concise than extension methods. For instance, we only needed
to define the length and apply methods in the stringWrapper conversion,
and we got all other methods in RandomAccessSeq for free. With extension
methods you’d need to define every one of these methods again. This dupli-
cation makes code harder to write, and, more importantly, harder to maintain.
Imagine someone adds a new method to RandomAccessSeq sometime in the
future. If all you have is extension methods, you’d have to chase down all
RandomAccessSeq “copycats” one by one, and add the new method in each.
If you forget one of the copycats, your system would become inconsistent.
Talk about a maintenance nightmare! By contrast, with Scala’s implicits, all
conversions would pick up the newly added method automatically.
Another advantage of implicit conversions is that they support conver-
sions into the target type, a type that’s needed at some point in the code. For
instance, suppose you write a method printWithSpaces, which prints all
characters in a given random access sequence with spaces in between them:
def printWithSpaces(seq: RandomAccessSeq[Char]) =
seq mkString " "
Because Strings are implicitly convertible to RandomAccessSeqs, you can
pass a string to printWithSpaces:
scala> printWithSpaces("xyz")
res2: String = x y z
The last expression is equivalent to the following one, where the conversion
shows up explicitly:
scala> printWithSpaces(stringWrapper("xyz"))
res3: String = x y z
This section has shown you some of the power of implicit conversions,
and how they let you “dress up” existing libraries. In the next sections you’ll
learn the rules that determine when implicit conversions are tried and how
they are found.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 21.2 Chapter 21 · Implicit Conversions and Parameters 468
21.2 Rules for implicits
Implicit definitions are those that the compiler is allowed to insert into a
program in order to fix any of its type errors. For example, if x + y does
not type check, then the compiler might change it to convert(x) + y, where
convert is some available implicit conversion. If convert changes x into
something that has a + method, then this change might fix a program so that it
type checks and runs correctly. If convert really is just a simple conversion
function, then leaving it out of the source code can be a clarification.
Implicit conversions are governed by the following general rules:
Marking Rule: Only definitions marked implicit are available. The
implicit keyword is used to mark which declarations the compiler may
use as implicits. You can use it to mark any variable, function, or object
definition. Here’s an example of an implicit function definition:
2
implicit def intToString(x: Int) = x.toString
The compiler will only change x + y to convert(x) + y if convert is marked
as implicit. This way, you avoid the confusion that would result if the
compiler picked random functions that happen to be in scope and inserted
them as “conversions. The compiler will only select among the definitions
you have explicitly marked as implicit.
Scope Rule: An inserted implicit conversion must be in scope as a single
identifier, or be associated with the source or target type of the conver-
sion. The Scala compiler will only consider implicit conversions that are
in scope. To make an implicit conversion available, therefore, you must in
some way bring it into scope. Moreover, with one exception, the implicit
conversion must be in scope as a single identifier. The compiler will not in-
sert a conversion of the form someVariable.convert. For example, it will
not expand x + y to someVariable.convert(x) + y. If you want to make
someVariable.convert available as an implicit, therefore, you would need
to import it, which would make it available as a single identifier. Once im-
ported, the compiler would be free to apply it as convert(x) + y. In fact, it
is common for libraries to include a Preamble object including a number of
2
Variables and singleton objects marked implicit can be used as implicit parameters.
This use case will be described later in this chapter.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 21.2 Chapter 21 · Implicit Conversions and Parameters 469
useful implicit conversions. Code that uses the library can then do a single
import Preamble._” to access the library’s implicit conversions.
There’s one exception to the “single identifier” rule. The compiler will
also look for implicit definitions in the companion object of the source or
expected target types of the conversion. For example, if you’re attempting
to pass a Dollar object to a method that takes a Euro, the source type is
Dollar and the target type is Euro. You could, therefore, package an implicit
conversion from Dollar to Euro in the companion object of either class,
Dollar or Euro. Here’s an example in which the implicit definition is placed
in Dollars companion object:
object Dollar {
implicit def dollarToEuro(x: Dollar): Euro = ...
}
class Dollar { ... }
In this case, the conversion dollarToEuro is said to be associated to the
type Dollar. The compiler will find such an associated conversion every
time it needs to convert from an instance of type Dollar. There’s no need to
import the conversion separately into your program.
The Scope Rule helps with modular reasoning. When you read code in
a file, the only things you need to consider from other files are those that are
either imported or are explicitly referenced through a fully qualified name.
This benefit is at least as important for implicits as for explicitly written code.
If implicits took effect system-wide, then to understand a file you would have
to know about every implicit introduced anywhere in the program!
Non-Ambiguity Rule: An implicit conversion is only inserted if there is
no other possible conversion to insert. If the compiler has two options
to fix x + y, say using either convert1(x) + y or convert2(x) + y, then it
will report an error and refuse to choose between them. It would be possible
to define some kind of “best match” rule that prefers some conversions over
others. However, such choices lead to really obscure code. Imagine the
compiler chooses convert2, but you are new to the file and are only aware
of convert1—you could spend a lot of time thinking a different conversion
had been applied!
In cases like this, one option is to remove one of the imported implicits
so that the ambiguity is removed. If you prefer convert2, then remove the
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 21.2 Chapter 21 · Implicit Conversions and Parameters 470
import of convert1. Alternatively, you can write your desired conversion
explicitly: convert2(x) + y.
One-at-a-time Rule: Only one implicit is tried. The compiler will never
rewrite x + y to convert1(convert2(x)) + y. Doing so would cause com-
pile times to increase dramatically on erroneous code, and it would increase
the difference between what the programmer writes and what the program
actually does. For sanity’s sake, the compiler does not insert further im-
plicit conversions when it is already in the middle of trying another implicit.
However, it’s possible to circumvent this restriction by having implicits take
implicit parameters, which will be described later in this chapter.
Explicits-First Rule: Whenever code type checks as it is written, no
implicits are attempted. The compiler will not change code that already
works. A corollary of this rule is that you can always replace implicit iden-
tifiers by explicit ones, thus making the code longer but with less apparent
ambiguity. You can trade between these choices on a case-by-case basis.
Whenever you see code that seems repetitive and verbose, implicit conver-
sions can help you decrease the tedium. Whenever code seems terse to the
point of obscurity, you can insert conversions explicitly. The amount of im-
plicits you leave the compiler to insert is ultimately a matter of style.
Naming an implicit conversion. Implicit conversions can have arbitrary
names. The name of an implicit conversion matters only in two situations: if
you want to write it explicitly in a method application, and for determining
which implicit conversions are available at any place in the program.
To illustrate the second point, say you have an object with two implicit
conversions:
object MyConversions {
implicit def stringWrapper(s: String):
RandomAccessSeq[Char] = ...
implicit def intToString(x: Int): String = ...
}
In your application, you want to make use of the stringWrapper conver-
sion, but you don’t want integers to be converted automatically to strings by
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index