Implicit conversions

The first type of implicit in our list is implicit conversion. They allow you automatically to convert values of one type into values of another type. This implicit conversion is defined as a one-argument method that's marked with the implicit keyword. Implicit conversions are considered to be a somewhat controversial language feature (we will take a look at why in a moment), so we need to enable them explicitly with a compiler flag or by importing the corresponding language feature:

import scala.language.implicitConversions

Predef contains a number of implicit conversions for Java-specific classes and primitives. For example, this is how autoboxing and autounboxing is defined for Scala's Int and Java's Integer:

// part of Predef in Scala
implicit def int2Integer(x: Int): java.lang.Integer = x.asInstanceOf[java.lang.Integer]
implicit def Integer2int(x: java.lang.Integer): Int = x.asInstanceOf[Int]

These two methods are used by the compiler in cases where a value of type Int is expected, but the value with the type java.lang.Integer is provided and vice versa. Assuming that we have a Java method returning a random Integer, we would have implicit conversion applied in the following scenario:

val integer: Integer = RandomInt.randomInt()
val int: Int = math.abs(integer)

math.abs expects Int, but an Integer is provided, so the compiler applies the implicit conversion Integer2int.

Identical principles apply to the return types in the same way that they apply to the parameters. If the compiler finds a method call on a type that does not have this method, it will look for an implicit conversion so that the original return type can be converted to the type that suits this method. This allows you to implement a pattern called extension methods. A String type in Scala is a perfect example. It is defined as a type alias for Java's String:

type String = java.lang.String

But it is possible to call methods such as map, flatMap, append, prepend, and many others, which are not defined in the original String. This is achieved by converting a String into StringOps every time such a method is called:

@inline implicit def augmentString(x: String): StringOps = new StringOps(x)

scala> "I'm a string".flatMap(_.toString * 2) ++ ", look what I can do"
res1: String = II''mm aa ssttrriinngg, look what I can do

The implicit conversion can be type-parameterized, but cannot be nested or directly chained. The compiler will only apply one implicit conversion at a time:

case class A[T](a: T)
case class B[T](a: T)

implicit def a2A[T](a: T): A[T] = A(a)
implicit def a2B[T](a: T): B[T] = B(a)

def ab[C](a: B[A[C]]): Unit = println(a)

The compiler will accept the call with A because of implicit conversion t2B being in scope, but will reject everything that is neither A nor B:

scala> ab(A("A"))
B(A(A))

scala> ab("A")
^
error: type mismatch;
found : String("A")
required: B[A[?]]

Sometimes, it is possible to enforce one of the conversions so that the compiler can then apply the other. Here, we tell the compiler to apply a conversion from String to A[String] by providing a type ascription. The conversion from A to B[A] then happens like it did previously:

scala> ab("A" : A[String])
B(A(A))

Quite handy, isn't it?

Why are implicit conversions considered disputable, then? Because sometimes they can be applied without the developer knowing that and can change semantics in unexpected ways. This can be especially bad in situations where conversions for two types exist for both directions (like in our Int/Integer example) or when pre-existing types are involved. This classical example is based on having some implicit conversions in scope and type coercions later:

scala> implicit val directions: List[String] = List("North", "West", "South", "East")
directions: List[String] = List(north, west, south, east)
scala> implicit val grades: Map[Char, String] = Map('A' -> "90%", 'B' -> "80%", 'C' -> "70%", 'D' -> "60%", 'F' -> "0%")
grades: Map[Char,String] = ChampHashMap(F -> 0%, A -> 90%, B -> 80%, C -> 70%, D -> 60%)
scala> println("B" + 42: String)
B42
scala> println(("B" + 42): String)
B42
scala> println("B" + (42: String))
java.lang.IndexOutOfBoundsException: 42
at scala.collection.LinearSeqOps.apply(LinearSeq.scala:74)
at scala.collection.LinearSeqOps.apply$(LinearSeq.scala:71)
at scala.collection.immutable.List.apply(List.scala:72)
... 38 elided
scala> "B" + 'C'
res3: String = BC
scala> "B" + ('C': String)
res4: String = B70%
scala> "B" + (2: String)
res5: String = BSouth

Here, we can see two examples of this behavior: one with a semantically similar String plus Int concatenation producing different results, and another crafted in the same way but for String and Char.

The reason for strange results and IndexOutOfBoundsException is that Map and List both implement PartialFunction, and thus just Function1. In our case, it's Int => String for the List and Char => String for the Map. Both are defined as implicit, and at the moment one of both type conversions is required, the corresponding function is applied. 

Because of this unpredictability, the use of implicit conversions is discouraged in modern Scala, though they are not removed from the language or deprecated, because a lot of existing implementations depend on them. They are mostly used to add methods to the existing classes or to add trait implementations for the new traits.

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

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