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

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

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

Добавлен: 02.01.2026

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

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

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

Section 21.2

Chapter 21 · Implicit Conversions and Parameters

484

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 identifiers 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 conversions can help you decrease the tedium. Whenever code seems terse to the point of obscurity, you can insert conversions explicitly. The amount of implicits 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):

IndexedSeq[Char] = ...

implicit def intToString(x: Int): String = ...

}

In your application, you want to make use of the stringWrapper conversion, but you don’t want integers to be converted automatically to strings by means of the intToString conversion. You can achieve this by importing only one conversion, but not the other:

import MyConversions.stringWrapper

... // code making use of stringWrapper

In this example, it was important that the implicit conversions had names, because only that way could you selectively import one and not the other.

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


Section 21.3

Chapter 21 · Implicit Conversions and Parameters

485

Where implicits are tried. There are three places implicits are used in the language: conversions to an expected type, conversions of the receiver of a selection, and implicit parameters. Implicit conversions to an expected type let you use one type in a context where a different type is expected. For example, you might have a String and want to pass it to a method that requires an IndexedSeq[Char]. Conversions of the receiver let you adapt the receiver of a method call, i.e., the object on which a method is invoked, if the method is not applicable on the original type. An example is "abc".exists, which is converted to stringWrapper("abc").exists because the exists method is not available on Strings but is available on IndexedSeqs. Implicit parameters, on the other hand, are usually used to provide more information to the called function about what the caller wants. Implicit parameters are especially useful with generic functions, where the called function might otherwise know nothing at all about the type of one or more arguments. Each of the following three sections will discuss one of these three kinds of implicits.

21.3 Implicit conversion to an expected type

Implicit conversion to an expected type is the first place the compiler will use implicits. The rule is simple. Whenever the compiler sees an X, but needs a Y, it will look for an implicit function that converts X to Y. For example, normally a double cannot be used as an integer, because it loses precision:

scala> val i: Int = 3.5 <console>:4: error: type mismatch;

found : Double(3.5) required: Int

val i: Int = 3.5

ˆ

However, you can define an implicit conversion to smooth this over:

scala> implicit def doubleToInt(x: Double) = x.toInt doubleToInt: (x: Double)Int

scala> val i: Int = 3.5 i: Int = 3

What happens here is that the compiler sees a Double, specifically 3.5, in a context where it requires an Int. So far, the compiler is looking at an

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

Section 21.4

Chapter 21 · Implicit Conversions and Parameters

486

ordinary type error. Before giving up, though, it searches for an implicit conversion from Double to Int. In this case, it finds one: doubleToInt, because doubleToInt is in scope as a single identifier. (Outside the interpreter, you might bring doubleToInt into scope via an import or possibly through inheritance.) The compiler then inserts a call to doubleToInt automatically. Behind the scenes, the code becomes:

val i: Int = doubleToInt(3.5)

This is literally an implicit conversion. You did not explicitly ask for conversion. Instead, you marked doubleToInt as an available implicit conversion by bringing it into scope as a single identifier, and then the compiler automatically used it when it needed to convert from a Double to an Int.

Converting Doubles to Ints might raise some eyebrows, because it’s a dubious idea to have something that causes a loss in precision happen invisibly. So this is not really a conversion we recommend. It makes much more sense to go the other way, from some more constrained type to a more general one. For instance, an Int can be converted without loss of precision to a Double, so an implicit conversion from Int to Double makes sense. In fact, that’s exactly what happens. The scala.Predef object, which is implicitly imported into every Scala program, defines implicit conversions that convert “smaller” numeric types to “larger” ones. For instance, you will find in Predef the following conversion:

implicit def int2double(x: Int): Double = x.toDouble

That’s why in Scala Int values can be stored in variables of type Double. There’s no special rule in the type system for this; it’s just an implicit conversion that gets applied.2

21.4 Converting the receiver

Implicit conversions also apply to the receiver of a method call, the object on which the method is invoked. This kind of implicit conversion has two main uses. First, receiver conversions allow smoother integration of a new class into an existing class hierarchy. And second, they support writing domainspecific languages (DSLs) within the language.

2The Scala compiler backend will treat the conversion specially, however, translating it to a special “i2d” bytecode. So the compiled image is the same as in Java.

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


Section 21.4

Chapter 21 · Implicit Conversions and Parameters

487

To see how it works, suppose you write down obj.doIt, and obj does not have a member named doIt. The compiler will try to insert conversions before giving up. In this case, the conversion needs to apply to the receiver, obj. The compiler will act as if the expected “type” of obj were “has a member named doIt.” This “has a doIt” type is not a normal Scala type, but it is there conceptually and is why the compiler will insert an implicit conversion in this case.

Interoperating with new types

As mentioned previously, one major use of receiver conversions is allowing smoother integration of new with existing types. In particular, they allow you to enable client programmers to use instances of existing types as if they were instances of your new type. Take, for example, class Rational shown in Listing 6.5 on page 155. Here’s a snippet of that class again:

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

...

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

def + (that: Int): Rational = ...

}

Class Rational has two overloaded variants of the + method, which take Rationals and Ints, respectively, as arguments. So you can either add two rational numbers or a rational number and an integer:

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

scala> oneHalf + oneHalf res0: Rational = 1/1

scala> oneHalf + 1 res1: Rational = 3/2

What about an expression like 1 + oneHalf, however? This expression is tricky because the receiver, 1, does not have a suitable + method. So the following gives an error:

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

Section 21.4

Chapter 21 · Implicit Conversions and Parameters

488

scala> 1 + oneHalf

<console>:6: error: overloaded method value + with alternatives (Double)Double <and> ... cannot be applied to (Rational)

1 + oneHalf

ˆ

To allow this kind of mixed arithmetic, you need to define an implicit conversion from Int to Rational:

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

intToRational: (x: Int)Rational

With the conversion in place, converting the receiver does the trick:

scala> 1 + oneHalf res2: Rational = 3/2

What happens behind the scenes here is that Scala compiler first tries to type check the expression 1 + oneHalf as it is. This fails because Int has several + methods, but none that takes a Rational argument. Next, the compiler searches for an implicit conversion from Int to another type that has a + method which can be applied to a Rational. It finds your conversion and applies it, which yields:

intToRational(1) + oneHalf

In this case, the compiler found the implicit conversion function because you entered its definition into the interpreter, which brought it into scope for the remainder of the interpreter session.

Simulating new syntax

The other major use of implicit conversions is to simulate adding new syntax. Recall that you can make a Map using syntax like this:

Map(1 -> "one", 2 -> "two", 3 -> "three")

Have you wondered how the -> is supported? It’s not syntax! Instead, -> is a method of the class ArrowAssoc, a class defined inside the standard Scala

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



Section 21.5

Chapter 21 · Implicit Conversions and Parameters

489

preamble (scala.Predef). The preamble also defines an implicit conversion from Any to ArrowAssoc. When you write 1 -> "one", the compiler inserts a conversion from 1 to ArrowAssoc so that the -> method can be found. Here are the relevant definitions:

package scala object Predef {

class ArrowAssoc[A](x: A) {

def -> [B](y: B): Tuple2[A, B] = Tuple2(x, y)

}

implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] = new ArrowAssoc(x)

...

}

This “rich wrappers” pattern is common in libraries that provide syntax-like extensions to the language, so you should be ready to recognize the pattern when you see it. Whenever you see someone calling methods that appear not to exist in the receiver class, they are probably using implicits. Similarly, if you see a class named RichSomething, e.g., RichInt or RichBoolean, that class is likely adding syntax-like methods to type Something.

You have already seen this rich wrappers pattern for the basic types described in Chapter 5. As you can now see, these rich wrappers apply more widely, often letting you get by with an internal DSL defined as a library where programmers in other languages might feel the need to develop an external DSL.

21.5 Implicit parameters

The remaining place the compiler inserts implicits is within argument lists. The compiler will sometimes replace someCall(a) with someCall(a)(b), or new SomeClass(a) with new SomeClass(a)(b), thereby adding a missing parameter list to complete a function call. It is the entire last curried parameter list that’s supplied, not just the last parameter. For example, if someCall’s missing last parameter list takes three parameters, the compiler might replace someCall(a) with someCall(a)(b, c, d). For this usage, not only must the inserted identifiers, such as b, c, and d in (b, c, d), be

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