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.
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.
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
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.
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
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.
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
.
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]) =
...
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
.
for
and match
ExpressionsYou 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)
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 nameimport 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
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")
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:
In the scope of the call—that is, in enclosing blocks and types, in supertypes, and in imports.
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.
In all given instances that are available at the point of the call.
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
.
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.
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
.
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.
Implicit conversions are considered in two distinct situations.
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
.
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.
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
.
The compiler will never attempt multiple conversions, such as convert1(convert2(a)) * b
.
Ambiguous conversions are an error. For example, if both convert1(a) * b
and convert2(a) * b
are valid, the compiler will report an error.
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.
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.
Scala will consider the following implicit conversion functions:
Implicit functions or classes in the companion object of the source or target type
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
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.
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
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
.
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.
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
.
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.
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]()
@implicitNotFound
AnnotationThe @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.
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
.
3.141.104.67