Implicit parameters

Implicit parameters use the same syntax as implicit conversions, but provide different functionality. They allow you to pass arguments into a function automatically . The definition of implicit parameters is done as a separate argument list in the definition of the function with a leading implicit keyword. Only one implicit argument list is allowed:

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

def
ab[C](name: String)(a: A[C])(implicit b: B[C]): Unit =
println(s"$name$a$b")

Implicit arguments do not require any special imports or compiler options to be activated. The preceding example shows that they also can be type-parameterized. If there is no value for the implicit argument visible at the moment the method is called, the compiler will report an error:

scala> ab("1")(A("A"))
^
error: could not find implicit value for parameter b: B[String]

This error can be fixed by providing the required implicit value:

scala> implicit val b = B("[Implicit]")
b: B[String] = B([Implicit])

scala> ab("1")(A("A"))
1A(A)B([Implicit])

If there are multiple implicit values in scope, the compiler will return an error:

scala> implicit val c = B("[Another Implicit]")
c: B[String] = B([Another Implicit])

scala> ab("1")(A("A"))
^
error: ambiguous implicit values:
both value b of type => B[String]
and value c of type => B[String]
match expected type B[String]

The solution to this problem is to remove all but one of the ambiguous implicit values or make one of the values more specific. We will look at how this can be done in a moment. Yet another approach is to provide the implicit value explicitly:

scala> ab("1")(A("A"))(b)
1A(A)B([Implicit])

scala> ab("1")(A("A"))(c)
1A(A)B([Another Implicit])

The implicit parameter does not need to be a value—it can be defined as a method. Having impure implicit methods can lead to random behavior, especially in the case of the type of implicit parameter being somewhat general:

scala> implicit def randomLong: Long = scala.util.Random.nextLong()
randomLong: Long

scala> def withTimestamp(s: String)(implicit time: Long): Unit = println(s"$time: $s")
withTimestamp: (s: String)(implicit time: Long)Unit

scala> withTimestamp("First")
-3416379805929107640: First

scala> withTimestamp("Second")
8464636473881709888: Second

Because of this, there is a general rule that implicit parameters must have possibly specific types. Following this rule also allows you to avoid confusing the compiler with recursive implicit parameters like the following:

scala> implicit def recursiveLong(implicit seed: Long): Long = scala.util.Random.nextLong(seed)
recursiveLong: (implicit seed: Long)Long

scala> withTimestamp("Third")
^
error: diverging implicit expansion for type Long
starting with method recursiveLong

Done right, implicit parameters can be very useful and can provide configuration parameters for the implementations. Usually, it is done top-down and affects all layers of the program:

object Application {
case class Configuration(name: String)
implicit val cfg: Configuration = Configuration("test")
class Persistence(implicit cfg: Configuration) {
class Database(implicit cfg: Configuration) {
def query(id: Long)(implicit cfg: Configuration) = ???
def update(id: Long, name: String)(implicit cfg: Configuration) = ???
}
new Database().query(1L)
}
}

In this example, the configuration is defined once in the top layer and automatically passed down to the methods in the lowest layer. As a result, the calling of the function becomes more readable.

These configuration settings are just a special case of a more general use case—context passing. The context is usually stable compared to normal arguments and it is because of this that it makes sense to pass it implicitly. The classic example of this is an ExecutionContext, which is required for most of the Future methods (we'll take a detailed look at this in Chapter 6Exploring Built-In Effects):

def filter(p: T => Boolean)(implicit executor: ExecutionContext): Future[T] = ...

The execution context usually doesn't change as opposed to the filtering logic, and therefore is passed implicitly.

Another use case is to verify types. We already saw an example of this in Chapter 2Understanding Types in Scala when we discussed generalized type constraints.

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

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