Chapter 19. Contextual Abstractions

Topics in This Chapter L3

The Scala language provides a number of powerful tools for carrying out work that depends on a context—settings that are configured depending on current needs. In this chapter, you will learn how to build capabilities that can be added in an ad-hoc fashion to class hierarchies, how to enrich existing classes, and how to carry out automatic conversions. With contextual abstractions, you can provide elegant mechanisms that hide tedious details from users of your code.

The key points of this chapter are:

  • A context parameter requests a given object of a specific type. You can define suitable given objects for different contexts.

  • The summon[T] function summons the current given object for type T.

  • It can be obtained from implicit objects that are in scope, or from the companion object of the desired type.

  • There is special syntax for declaring given objects that extend a trait and define methods.

  • You can build complex given objects, using type parameters and context bounds.

  • If an implicit parameter is a function with a single parameter, it is also used as an implicit conversion.

  • Typically, you use import statements to supply the given instances that you need.

  • Using extension methods, you can add methods to existing classes.

  • Implicit conversions are used to convert between types.

  • You control which extensions and implicit conversions are available at a particular point.

  • A context function type describes a function with context parameters.

19.1 Context Parameters

Typically, systems have certain settings that are needed by many functions—such as a database connection, a logger, or a locale. Passing those settings as arguments is impractical, because every function would need to pass them to the functions that it calls. You can use globally visible objects, but that is inflexible. Scala has a powerful mechanism to make values available to functions without explicitly passing them—through context parameters. Let’s first understand how a function receives those parameters.

A function or method can have one or more parameter lists marked with the using keyword. For those parameters, the compiler will look for appropriate values to supply with the function call, using a mechanism that we will study later. Here is a simple example:

case class QuoteDelimiters(left: String, right: String)

def quote(what: String)(using delims: QuoteDelimiters) =
  delims.left + what + delims.right

You can call the quote method with an explicit QuoteDelimiters object, like this:

quote("Bonjour le monde")(using QuoteDelimiters("«", "»"))
// Returns «Bonjour le monde»

Note that there are two argument lists. This function is “curried”—see Chapter 12.

However, the point of context parameters is that you don’t want to provide an explicit using parameter. You simply want to call:

quote("Bonjour le monde")

Now the compiler will look for a suitable value of type QuoteDelimiters. This must be a value that is declared as given:

given englishQuoteDelims: QuoteDelimiters = QuoteDelimiters("“", "”")

Now the delimiters are supplied implicitly to the quote function.

Images Note

As you will see soon, there are more compact ways of declaring given values. Right now, I am using a form that is easy to understand.

However, you typically don’t want to declare global given values. In order to switch between contexts, you can declare objects such as the following:

object French :
  given quoteDelims: QuoteDelimiters = QuoteDelimiters("«", "»")
  ...

object German :
  given quoteDelims: QuoteDelimiters = QuoteDelimiters("„", "“")
  ...

Then import the given values from one such object, using the following syntax:

import German.given

Images Note

For each type, there can only be one given value. Thus, it is not a good idea to have using parameters of common types. For example,

def quote(what: String)(using left: String, right: String) // No!

would not work—there could not be two different given values of type String.

You have now seen the essence of context parameters. In the next three sections, you will learn about the finer points of using clauses, the syntax of given declarations, and the ways to import them.

19.2 More about Context Parameters

In the preceding section, you saw a function with a using parameter:

def quote(what: String)(using delims: QuoteDelimiters) = ...

If the function body doesn’t refer to the using parameter, you don’t have to give it a name:

def attributedQuote(who: String, what: String)(using QuoteDelimiters) =
  s"$who said: ${quote(what)}"

The using parameter still gets passed to the functions that need it—in this case, the quote function.

You can always obtain a given value by calling summon:

def quote(what: String)(using QuoteDelimiters) =
  summon[QuoteDelimiters].left + what + summon[QuoteDelimiters].right

By the way, here is the definition of summon:

def summon[T](using x: T): x.type = x

Images Note

Just as Shakespeare’s “vasty deep” is filled with spirits that any man can summon, the Scala nether world is populated with given objects, at most one for each type. Any programmer can summon them. Will they come? That depends on whether the compiler can locate a unique given object of the summoned type.

A function can have more than one using parameter. For example, we may want to localize messages in addition to quotes. Each language maps keys to MessageFormat templates:

case class Messages(templates: Map[String, String])

object French :
  ...
  given msgs: Messages =
    Messages(Map("greeting" -> "Bonjour {0}!",
      "attributedQuote" -> "{0} a dit {1}{2}{3}"))

This method receives the quote delimiters and message map through using parameters:

def attributedQuote(who: String, what: String)(
  using delims: QuoteDelimiters, msgs: Messages) =
    MessageFormat.format(msgs.templates("attributedQuote"),
      who, delims.left, what, delims.right)

You can curry the using parameters:


def attributedQuote(who: String, what: String)(
  using delims: QuoteDelimiters)(using msgs: Messages) = ...

With this example, there is no need to use currying. But there can be situations where currying helps with type inference.

A primary constructor can have context parameters:

class Quoter(using delims: QuoteDelimiters) :
  def quote(what: String) = ...
  def attributedQuote(who: String, what: String) = ...
  ...

The delims variable is initialized from the context parameter when the object is constructed. Afterwards, there is nothing special about it. Its value is accessible in all methods.

A using parameter can be declared by name (using ev: => T) to delay its production until it is needed. This can be useful if the given value is expensive to produce, or to break cycles. This is not common.

19.3 Declaring Given Instances

In this section, we go over the various ways of declaring given instances. You have already seen the explicit form with a name, a type, and a value.

given englishQuoteDelims: QuoteDelimiters = QuoteDelimiters("“", "”")

The right-hand side doesn’t have to be a constructor—it can be any expression of type QuoteDelimiters.

Images CAUTION

There is no type inference for given instances. The following is an error:

given englishQuoteDelims = QuoteDelimiters("“", "”")
  // Error—no type inference

If you use a constructor, you can omit the type and the = operator:

given englishQuoteDelims: QuoteDelimiters("“", "”")

You can also omit the name, since you don’t normally need it:

given QuoteDelimiters("“", "”")

Inside an abstract class or trait, you can define an abstract given, with a name and type but with no value. The value must be supplied in a subclass.

abstract class Service :
  given logger: Logger

It is common to declare given instances that override abstract methods. For example, when declaring a given instance of type Comparator[Int], you must provide an implementation of the compare method:

given intComp: Comparator[Int] =
  new Comparator[Int]() :
    def compare(x: Int, y: Int) = Integer.compare(x, y)

There is a convenient shortcut for this common case:

given intComp: Comparator[Int] with
  def compare(x: Int, y: Int) = Integer.compare(x, y)

Presumably, the with keyword was chosen to avoid having two colons in a row.

The name is optional:

given Comparator[Int] with
  def compare(x: Int, y: Int) = Integer.compare(x, y)

You can create parameterized given instances. For example:

given comparableComp[T <: Comparable[T]]: Comparator[T] with
    def compare(x: T, y: T) = x.compareTo(y)

The instance name is optional, but the colon before the instance type is required.

Let us use this example to introduce one more concept: given instances with using parameters. Consider the comparison of two List[T]. If neither are empty, we compare the heads.

How do we do that? We need to know that values of type T can be compared. That is, we need a given of type Comparator[T]. As always, this need can be expressed with a using clause:

given listComp[T](using tcomp: Comparator[T]): Comparator[List[T]] =
  new Comparator[List[T]]() :
    def compare(xs: List[T], ys: List[T]) =
      if xs.isEmpty && ys.isEmpty then 0
      else if xs.isEmpty then -1
      else if ys.isEmpty then 1
      else
        val diff = tcomp.compare(xs.head, ys.head)
        if diff != 0 then diff
        else compare(xs.tail, ys.tail)

As before, you can omit the name and take advantage of the with syntax:

given [T](using tcomp: Comparator[T]): Comparator[List[T]] with
  def compare(xs: List[T], ys: List[T]) =
    ...

Images Note

Instead of an explicit using parameter, you can use a context bound for the type parameter:

given [T : Comparator] : Comparator[List[T]] with ...

As you saw in Chapter 17, there must be a given Comparator[T] instance. That is equivalent to a using clause. However, there is one difference: You don’t have a parameter for the given instance. Instead, you summon it from the “nether world”:

val ev = summon[Comparator[T]]
val diff = ev.compare(xs.head, ys.head)

It is common to use the variable name ev for such summoned objects. It is shorthand for “evidence.” The fact that a given value can be summoned is evidence for their existence.

Let us reflect on the strategy that underlies the example. We declare ad-hoc given instances for Comparator[Int], Comparator[Double], and so on. Using a parameterized rule, we obtain Comparator[T] for all types T that extend Comparable[T]. This includes String and a large number of Java types such as LocalDate or Path. Next, we have Comparator[List[T]] instances of any type T that itself has a Comparator[T] instance. That process can be carried out for other collections and, with more technical effort, for tuples and case classes. Note that this happens without cooperation from individual classes. For example, we did not have to modify the List class in any way.

What is the benefit of this effort? It allows us to write generic functions that require an ordering. They will work with all types T with a given Comparator[T] instance. Here is a typical example:

def max[T](a: Array[T])(using comp: Comparator[T]) =
  if a.length == 0 then None
  else
    var m = a(0)
    for i <- 1 until a.length do
      if comp.compare(a(i), m) > 0 then m = a(i)
    Some(m)

The Scala library does just that, except with the Ordering trait that extends Comparator.

19.4 Givens in for and match Expressions

You can declare a given in a for loop:

val delims = List(QuoteDelimiters("“", "”"), QuoteDelimiters("«", "»"),
  QuoteDelimiters("„", "“"))
for given QuoteDelimiters <- delims yield
  quote(text)

In each iteration of the loop, the given QuoteDelimiters instance changes, and the quote function uses different delimiters.

In general, a for loop for x <- a yield f(x) is equivalent to a.map(f), in which the values of a become arguments of f. In the given form for given T <- a yield f(), the values of a become using arguments of f.

A pattern a match expression can be declared as given:

val p = (text, QuoteDelimiters("«", "»"))
p match
  case (t, given QuoteDelimiters) => quote(t)

The given value is introduced in the right-hand side of the matched case.

Note that in both situations, you specify the type of the given value, not the name of a variable. If you need a name, use the @ syntax:

for d @ given QuoteDelimiters <- delims yield
  quote(d.left + d.right)

p match
  case (t, d @ given QuoteDelimiters) => quote(d.left + d.right)

19.5 Importing Givens

Usually, you define given values in an object:

object French :
  given quoteDelims: QuoteDelimiters = QuoteDelimiters("«", "»")
  given NumberFormat = NumberFormat.getNumberInstance(Locale.FRANCE)

You import them as needed, either by name or by type:

import French.quoteDelims // imported by name
import French.given NumberFormat // imported by type

You can import multiple given values. Imports by name come before imports by type.

import French.{quoteDelims, given NumberFormat}

If the type is parameterized, you can import all given instances by using a wildcard. For example,

import Comparators.given Comparator[?]

imports all given Comparator instances from

object Comparators :
  def ≤[T : Comparator](x: T, y: T) = summon[Comparator[T]].compare(x, y) <= 0
  given Comparator[Int] with
    ...
  given [T <: Comparable[T]]: Comparator[T] with
    ...
  given [T : Comparator]: Comparator[List[T]] with
    ...

To import all given values, use

import Comparators.given

Note that the wildcard import

import Comparators.*

does not import any given values. To import everything, you need to use:

import Comparators.{*, given}

You can also export given values:

object CanadianFrench :
  given NumberFormat = NumberFormat.getNumberInstance(Locale.CANADA)
  export French.given QuoteDelimiters

19.6 Extension Methods

Did you ever wish that a class had a method its creator failed to provide? For example, wouldn’t it be nice if the java.io.File class had a read method for reading a file:

val contents = File("/usr/share/dict/words").read

As a Java programmer, your only recourse is to petition Oracle Corporation to add that method. Good luck!

In Scala, you can define an extension method that provides what you want:

extension (f: File)
  def read = Files.readString(f.toPath)

Now it is possible to call read on a File object.

An extension method can be an operator:

extension (s: String)
  def -(t: String) = s.replace(t, "")

Now you can subtract strings:

"units" - "u"

Extensions can be parameterized:

extension [T <: Comparable[? >: T]](x: T)
  def <(y: T) = x.compareTo(y) < 0

Note that this extension is selective: It adds a < method only to types extending Comparable.

You can add multiple extension methods to the same type:

extension (f: File)
  def read = Files.readString(f.toPath)
  def write(contents: String) = Files.writeString(f.toPath, contents)

An export clause can add multiple methods:

extension (f: File)
  def path = f.toPath
  export path.*

Now all Path methods are applicable to File objects:

val home = File("")
home.toAbsolutePath() // On my computer, that is Path("/home/cay")

19.7 Where Extension Methods Are Found

Consider a method call

obj.m(args)

where m is not defined for obj. Then the Scala compiler needs to look for an extension method named m that can be applied to obj.

There are four places where the compiler looks for extension methods:

  1. In the scope of the call—that is, in enclosing blocks and types, in supertypes, and in imports.

  2. In the companion objects of all types that make up the type T of obj. For historical reasons, this is called the “implicit scope” of the type T. For example, the implicit scope of Pair[Person] consists of the members of the Pair and Person objects.

  3. In all given instances that are available at the point of the call.

  4. In all given instances in the implicit scope of the type of obj.

Let us look at examples of each of these situations.

You can place extension methods in an object or package, and then import it:

object FileOps : // or package
  extension(f: File)
    def read = Files.readString(f.toPath)

import FileOps.* // or import FileOps.read
File("/usr/share/dict/words").read

You can put extension methods in a trait or class and extend it:

trait FileOps :
  extension(f: File)
    def read = Files.readString(f.toPath)

class Task extends FileOps :
  def call() =
    File("/usr/share/dict/words").read

Here is an example of an extension method in a companion object:

case class Pair[T](first: T, second: T)

object Pair :
  extension [T](pp: Pair[Pair[T]])
    def zip = Pair(Pair(pp.first.first, pp.second.first),
      Pair(pp.first.second, pp.second.second))

val obj = Pair(Pair(1, 2), Pair(3, 4))
obj.zip // Pair(Pair(1, 3), Pair(2, 4))

The zip method can’t be a method of the Pair class. It doesn’t work for arbitrary pairs, but only for pairs of pairs. Since the zip method is invoked on an instance of Pair[Pair[Int]], both the Pair and Int companion objects are searched for an extension method.

Now let us look at extension methods in given values. Consider a stringify method that formats numbers, strings, arrays, and objects using JSON. It has to be an extension method since there is not currently such a method for numbers, strings, pairs, or arrays.

Moreover, we need a way of expressing the requirement that the elements of pairs or arrays can be formatted. As with the Comparator example in Section 19.3, “Declaring Given Instances,” on page 299, we will use a context bound.

The following JSON trait requires the existence of stringify as an extension method. A companion object declares given values JSON[Double] and JSON[String]:

trait JSON[T] :
  extension (t: T) def stringify: String

object JSON :
  given JSON[Double] with
    extension (x: Double)
      def stringify = x.toString
  def escape(s: String) = s.flatMap(
    Map('' -> "\\", '"' -> "\"", '
' -> "\n", '
' -> "\r").withDefault(
      _.toString))
  given JSON[String] with
    extension (s: String)
      def stringify = s""""${escape(s)}""""

Now let’s define a suitable extension method for Pair[T], provided that T can be formatted:

object Pair :
  given [T : JSON]: JSON[Pair[T]] with
    extension (p: Pair[T])
      def stringify: String =
        s"""{"first": ${p.first.stringify}, "second": ${p.second.stringify}}"""

Have a close look at the expression p.first.stringify. How does the compiler know that p.first has a stringify method? The type of p.first is T. Because of the context bound, there is a given object of type JSON[T]. That given object defines an extension method for the type T with name stringify. You have just seen an example of rule #3 for locating extension methods.

Finally, for rule #4, consider the call Pair(1.7, 2.9).stringify. The extension method is not declared in the Pair companion object but in a given value that is declared in Pair.

19.8 Implicit Conversions

An implicit conversion is a function with a single parameter that is a given instance of Conversion[S, T] (which extends the type S => T). As the name suggests, such a function is automatically applied to convert values from the source type S to the target type T.

Consider the Fraction class from Chapter 11. We want to convert integers n to fractions n / 1.

given int2Fraction: Conversion[Int, Fraction] = n => Fraction(n, 1)

Now we can evaluate

val result = 3 * Fraction(4, 5) // Calls int2Fraction(3)

The implicit conversion turns the integer 3 into a Fraction object. That object is then multiplied by Fraction(4, 5).

You can give any name, or no name at all, to the conversion function. Since you don’t call it explicitly, you may be tempted to drop the name. But, as you will see in Section 19.10, “Importing Implicit Conversions,” on page 308, sometimes it is useful to import a conversion function. I suggest that you stick with the source2target convention.

Scala is not the first language that allows the programmer to provide automatic conversions. However, Scala gives programmers a great deal of control over when to apply these conversions. In the following sections, we will discuss exactly when the conversions happen and how you can fine-tune the process.

Images Note

Even though Scala gives you tools to fine-tune implicit conversions, the language designers realize that implicit conversions are potentially problematic. To avoid a warning when using implicit functions, add the statement import scala.language.implicitConversions or the compiler option -language:implicitConversions.

Images Note

In C++, you specify an implicit conversion as a constructor with a single parameter or a member function with the name operator Type(). However, in C++, you cannot selectively allow or disallow these functions, and it is common to run into unwanted conversions.

19.9 Rules for Implicit Conversions

Implicit conversions are considered in two distinct situations.

  1. If the type of an argument differs from the expected type:

    Fraction(3, 4) * 5 // Calls int2Fraction(5)

    The * method of Fraction doesn’t accept an Int but it accepts a Fraction.

  2. If an object accesses a nonexistent member:

    3 * Fraction(4, 5) // Calls int2Fraction(3)

    The Int class doesn’t have a *(Fraction) member but the Fraction class does.

On the other hand, there are some situations when an implicit conversion is not attempted.

  1. No implicit conversion is used if the code compiles without it. For example, if a * b compiles, the compiler won’t try a * convert(b) or convert(a) * b.

  2. The compiler will never attempt multiple conversions, such as convert1(convert2(a)) * b.

  3. Ambiguous conversions are an error. For example, if both convert1(a) * b and convert2(a) * b are valid, the compiler will report an error.

Images CAUTION

Suppose we also have a conversion

given fraction2Double: Conversion[Fraction, Double] =
  f => f.num * 1.0 / f.den

It is not an ambiguity that

3 * Fraction(4, 5)

could be either

3 * fraction2Double(Fraction(4, 5))

or

int2Fraction(3) * Fraction(4, 5)

The first conversion wins over the second, since it does not require modification of the object to which the * method is applied.

Images Tip

If you want to find out which using parameters, extension methods, and implicit conversions the compiler uses, compile your program as

scalac -Xprint:typer MyProg.scala

You will see the source after these contextual mechanisms have been added.

19.10 Importing Implicit Conversions

Scala will consider the following implicit conversion functions:

  1. Implicit functions or classes in the companion object of the source or target type

  2. Implicit functions or classes that are in scope

For example, consider the int2Fraction and fraction2Double conversions. We can place them into the Fraction companion object, and they will be available for conversions involving fractions.

Alternatively, let’s suppose we put the conversions inside a FractionConversions object, which we define in the com.horstmann.impatient package. If you want to use the conversions, import the FractionConversions object like this:

import com.horstmann.impatient.FractionConversions.given

You can localize the import to minimize unintended conversions. For example,

@main def demo =
    import com.horstmann.impatient.FractionConversions.given
  val result = 3 * Fraction(4, 5) // Uses imported conversion fraction2Double
  println(result)

You can even select the specific conversions that you want. If you prefer int2Fraction over fraction2Double, you can import it specifically:


import com.horstmann.impatient.FractionConversions.int2Fraction
val result = 3 * Fraction(4, 5) // result is Fraction(12, 5)

You can also exclude a specific conversion if it causes you trouble:

import com.horstmann.impatient.FractionConversions.{fraction2Double as _, given}
  // Imports everything but fraction2Double

Images Tip

If you want to find out why the compiler doesn’t use an implicit conversion that you think it should use, try adding it explicitly, for example by calling fraction2Double(3) * Fraction(4, 5). You may get an error message that shows the problem.

19.11 Context Functions

Consider our first example of a function with a context parameter:

def quote(what: String)(using delims: QuoteDelimiters) =
  delims.left + what + delims.right

What is the type of quote? It can’t be

String => String

because there is that second parameter list. But clearly it’s not

String => QuoteDelimiters => String

either, because then we would call it as

quote(text)(englishQuoteDelims)

and not

quote(text)(using englishQuoteDelims)

There has to be some way of writing that type, and this is what the Scala designers came up with:

String => QuoteDelimiters ?=> String

Images Note

Perhaps you are surprised that the ? is with the second arrow. Recall that a curried function type T => S => R is really T => (S => R). When you fix the first parameter, you get a function S => R. In our case, fixing the text to a particular value, we have a function with one using parameter, whose type is denoted as QuoteDelimiters ?=> String.

Images Note

If a method takes only using parameters, such as

def randomQuote(using delims: QuoteDelimiters) = delims.left
  + scala.io.Source.fromURL(
      "https://horstmann.com/random/quote").mkString.split(" - ")(0)
  + delims.right

then its type has the form QuoteDelims ?=> String

Let us put this curious syntax to practical use. Suppose you develop a library where you work with the Future trait. In Chapter 16, we used the global ExecutionContext, but in practice, you will need to tag all your methods with (using ExecutionContext):

def GET(String url)(using ExecutionContext): Future[String] =
  Future {
    blocking {
      Source.fromURL(url).mkString
    }
  }

You can do slightly better than that by defining a type alias

type ContextualFuture[T] = ExecutionContext ?=> Future[T]

Then the method becomes

def GET(url: String): ContextualFuture[String] = ...

Here is a more interesting technique for implementing “magic identifiers.” I want to design an API for web requests where I can say:

GET("https://horstmann.com") { println(res) }

The first parameter of the GET method is a URL. The second is a handler—a block of code that is executed when the response is available. Note the magic identifier res that denotes the response string. Here is how the magic will unfold:

  • The GET method makes the HTTP request and obtains the response.

  • The GET method calls the handler, with the (wrapped) response as a using parameter.

  • res is a method that summons the response from the “vasty depth” of given objects.

We put everything into a Requests object and use a wrapper class for the given response, so that we don’t pollute the givens with an API class.

object Requests :
  class ResponseWrapper(val response: String)
  def res(using wrapper: ResponseWrapper): String = wrapper.response

The GET method has two curried parameters: the URL and the handler. The handler is a context function—note the ?=> arrow:

def GET(url: String)(handler: ResponseWrapper ?=> Unit) =
  Future {
    blocking {
      val response = Source.fromURL(url).mkString
      handler(using ResponseWrapper(response))
    }
  }

Note that the response is wrapped and passed to the handler as a using parameter. Then the handler code executes, and the res function retrieves and unwraps the response object.

This is not meant to be an exemplary design for an HTTP request library. It would be better to work with the Future API. The point of the example is that res is used as a “magic variable” that is available in the scope of the handler function.

See Exercise 12 on page 314 for another example of using context functions.

19.12 Evidence

In Chapter 17, you saw the type constraints

T =:= U
T <:< U

The constraints test whether T equals U or is a subtype of U. To use such a type constraint, you supply a using parameter, such as

def firstLast[A, C](it: C)(using ev: C <:< Iterable[A]): (A, A) =
  (it.head, it.last)

firstLast(0 until 10) // the pair (0, 9)

The =:= and <:< are classes that produce given values, defined in the Predef object. For example, <:< is essentially

abstract class <:<[-From, +To] extends Conversion[From, To]

object `<:<` :
  given conforms[A]: (A <:< A) with
    def apply(x: A) = x

Suppose the compiler processes a constraint using ev: Range <:< Iterable[Int]. It looks in the companion object for a given object of type Range <:< Iterable[Int]. Note that <:< is contravariant in From and covariant in To. Therefore the object

<:<.conforms[Range]

is usable as a Range <:< Iterable[Int] instance. (The <:<.conforms[Iterable[Int]] object is also usable, but it is less specific and therefore not considered.)

We call ev an “evidence object”—its existence is evidence of the fact that, in this case, Range is a subtype of Iterable[Int].

Here, the evidence object is the identity function. To see why the identity function is required, have a closer look at

def firstLast[A, C](it: C)(using ev: C <:< Iterable[A]): (A, A) =
  (it.head, it.last)

The compiler doesn’t actually know that C is an Iterable[A]—recall that <:< is not a feature of the language, but just a class. So, the calls it.head and it.last are not valid. But ev is a Conversion[C, Iterable[A]], and therefore the compiler applies it, computing ev(it).head and ev(it).last.

Images Tip

To test whether a given evidence object exists, you can call the summon function in the REPL. For example, type summon[Range <:< Iterable[Int]] in the REPL, and you get a result (which you now know to be a function). But summon[Iterable[Int] <:< Range] fails with an error message.

Images Note

If you need to check whether a given value does not exist, use the scala.util.NotGiven type:

given ts[T](using T <:< Comparable[? >: T]): java.util.Set[T] =
  java.util.TreeSet[T]()
given hs[T](using NotGiven[T <:< Comparable[? >: T]]): java.util.Set[T] =
  java.util.HashSet[T]()

19.13 The @implicitNotFound Annotation

The @implicitNotFound annotation raises an error message when the compiler cannot construct a given value of the annotated type. The intent is to give a useful error message to the programmer. For example, the <:< class is annotated as

@implicitNotFound(msg = "Cannot prove that ${From} <:< ${To}.")
abstract class <:<[-From, +To] extends Function1[From, To]

For example, if you call

firstLast[String, List[Int]](List(1, 2, 3))

then the error message is

Cannot prove that List[Int] <:< Iterable[String]

That is more likely to give the programmer a hint than the default

Could not find implicit value for parameter ev: <:<[List[Int],Iterable[String]]

Note that ${From} and ${To} in the error message are replaced with the type parameters From and To of the annotated class.

Exercises

1. Call summon in the REPL to obtain the given values described in Section 19.3, “Declaring Given Instances,” on page 299.

2. What is the difference between the following primary constructor context parameters?

class Quoter(using delims: QuoteDelimiters) : ...
class Quoter(val using delims: QuoteDelimiters) : ...
class Quoter(var using delims: QuoteDelimiters) : ...

3. In Section 19.3, “Declaring Given Instances,” on page 299, declare max with a context bound instead of a using parameter.

4. Show how a given instance of a Logger class can be used in a body of code, without having to pass it along in method calls. Compare with the usual practice of retrieving logger instances with calls such as Logger.getLogger(id).

5. Provide a given instance of Comparator that compares objects of the class java.awt.Point by lexicographic comparison.

6. Continue the previous exercise, comparing two points according to their distance to the origin. How can you switch between the two orderings?

7. Produce a given Ordering for Array[T] instances, provided that there is a given Ordering[T].

8. Produce a given JSON for Iterable[T] instances, provided that there is a given JSON[T]. Stringify as a JSON array.

9. Improve the GET method in Section 19.11, “Context Functions,” on page 309 so that it can retrieve bodies of type other than String. Use the Java HttpClient instead of scala.io.Source. Use a context bound that recognizes the (few) types for which there are body handlers.

10. Did you ever wonder how "Hello" -> 42 and 42 -> "Hello" can be pairs ("Hello", 42) and (42, "Hello")? Define your own operator that achieves this.

11. Define a ! operator that computes the factorial of an integer. For example, 5.! is 120.

12. Consider these function invocations for making an HTML table:

table {
  tr {
    td("Hello")
    td("Bongiorno")
  }
  tr {
    td("Goodbye")
    td("Arrividerci")
  }
}

A Table class stores rows:

class Table:
  val rows = new ArrayBuffer[Row]
  def add(r: Row) = rows += r
  override def toString = rows.mkString("<table>
", "
", "</table>")

Similarly, a Row stores data cells.

How do the Row instances get added to the correct Table? The trick is for the table method to introduce a given instance of type Table, and for the tr method to have a using parameter of type Table. Pass along a Row in the same way from the tr to the td method.

There is one technical complexity. The table and tr functions consume functions with no parameter, except that they need to pass along the using parameter. Their types are Table ?=> Any and Row ?=> Any (see Section 19.11, “Context Functions,” on page 309).

Complete the Table and Row classes and the table, tr, and td functions.

13. Improve the functions of the preceding exercise so that you can’t nest a table inside another table or a tr inside another tr. Hint: NotGiven.

14. Look up the =:= object in Predef.scala. Explain how it works.

15. What is the difference between the following classes?


class PairU[T](val first: T, val second: T) :
  def smaller(using ord: Ordering[T]) =
    if ord.compare(first, second) < 0 then first else second
class PairS[T : Ordering](val first: T, val second: T) :
  def smaller =
    if summon[Ordering[T]].compare(first, second) < 0 then first else second

Hint: Set different orderings when constructing instances and when invoking smaller.

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

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