In previous editions of this book, this chapter was titled Implicits after the mechanism used to implement many powerful idioms in Scala. Scala 3 begins the migration to new language constructs that emphasize purpose over mechanism, both to make learning and using these idioms easier and to address some shortcomings of the prior implementations. The transition will happen over several 3.X releases of Scala to make it easier, especially for existing code bases. Therefore, I will cover both the Scala 2 and 3 techniques, while emphasizing the latter.
All of these idioms fall under the umbrella abstracting over context. We saw a few examples already, such as the ExecutionContext
parameters needed in many Future
methods, discussed in ATasteOfFutures
. We’ll see many more idioms now in this chapter and the next. In all cases, the idea of “context” will be some situation where an extension to a type, a transformation to a new type, or an insertion of values automatically is desired for easier programming. Frankly, in all cases, it would be possible to live without the tools described here, but it would require more work on the user’s part. This raises an important point, though. Make sure you use these tools judiciously; all constructs have pros and cons.
The most sweeping changes introduced in Scala 3 are to the type system and to how we define and use context abstractions. The changes to the latter are designed to make the purpose and application of these abstractions more clear. The underlying implicit mechanism is still there, but it’s now easier to use for specific purposes. Not only do the changes make intention more clear, they also eliminate some boilerplate previously required when using implicits and fix other drawbacks of the Scala 2 idioms.
If you know Scala 2 implicits, the changes in Scala 3 can be summarized as follows:1
Instead of declaring implicit terms (i.e. val
s or methods) to be used to resolve implicit parameters, the new given
clause specifies how to synthesize the required term from a type. The change deemphasizes the previous distinction where you had to know when to declare an instance vs. a class. Most of the time you will specify that a particular class should be used to satisfy the need for an implicit value and the compiler does the rest.
Instead of using the keyword implicit
to declare an implicit parameter list for a method, you use the keyword using
. The different keyword eliminates several ambiguities and it allows a method definition to have more than one implicit parameter list, which are now called using clauses.
When you use wild cards in import statements, they no longer import given
instances along with everything else. Instead, you use the given
keyword to explicitly ask for givens to be imported.
For the special case of given terms that are used for implicit conversions between types, they are now declared as given instances of a standard Conversion
class. All other forms of implicit conversions will be phased out.
This chapter explores the context abstractions for extending types with additional state and behavior using extension methods and type classes, which are defined using given instances. We’ll also cover given imports and implicit conversions. In AbstractingOverContextPart2
, we’ll explore using clauses and the specific idioms they support.
In Scala 2, if we wanted to simulate adding new methods to existing types, we had to do an implicit conversion to a wrapper type that implements the method. Scala 3 adds extension methods that allow us to extend a type with new methods without conversion. By themselves, extension methods only allow us to add one or more methods, but not new fields for additional state, nor is there a mechanism for implementing a common abstraction. We’ll address those limitations when we discuss type classes.
But first, why not just modify the original source code? You may not have that option, for example if it’s a third-party library. Also, adding too many methods and fields to classes makes them very difficult to maintain. Keep in mind that every modification to an existing type forces users to recompile their code, at least. This is especially annoying if the changes involve functionality they don’t even use.
Context abstractions help us avoid the temptation to create types that contain lots of utility methods that are used only occasionally. Our types can avoid mixing concerns. For example, if some users want toJSON
methods on a hierarchy of types, like our Shape
s in ASampleApplication
, then only those users are affected.
Hence, the goal is to enable ad hoc additions to types in a principled way. By principled, type implementations can remain focused on their core abstractions, while additional behaviors can be added only where needed, as opposed to making global modifications that affect all users. These tools also preserve type safety.
However, a drawback of this separation of concerns is that the separate toJSON
functionality needs to track changes in the code for the type hierarchy. If a field is renamed, the compiler will catch it for us. If a new field is added, for example Shape.color
, it will be easy to miss.
Let’s explore an example. Recall that we used the pair construction idiom, a -> b
, to create tuples (a, b)
, which is popular for creating Map
instances:
val
map
=
Map
(
"one"
->
1
,
"two"
->
2
)
In Scala 2, this is done using an implicit conversion to a library type ArrowAssoc
in Predef
(some details omitted for simplicity):
implicit
final
class
ArrowAssoc
[
A
](
private
val
self
:
A
)
{
@infix
def
->
[
B
](
y
:
B
)
:
(
A
,
B
)
=
(
self
,
y
)
}
Here is how implicit conversion works in Scala 2. When the compiler sees the expression "one" -> 1
, it sees that String
does not have the ->
method. However, ArrowAssoc[T]
is in scope, it has this method, and the class is declared implicit
. So, the compiler can emit code to create an instance of ArrowAssoc[String]
, with the string "one"
passed as the self
argument, followed by code to call ->(1)
to construct and return the tuple ("one", 1)
.
If ArrowAssoc
were not declared implicit
, the compiler would not attempt to use it for this purpose.
Let’s re-implement this using a Scala 3 extension method. To avoid ambiguity with ->
, let’s use ~>
instead, but it works identically:
// src/script/scala/progscala3/contexts/ArrowAssocExtension.scala
scala
>
import
scala.annotation.
{
alpha
,
infix
}
scala
>
extension
[
A
,
B
]
(
a
:
A
)
:
|
@
infix
@
alpha
(
"
arrow2
"
)
def
~>
(
b
:
B
)
:
(
A
,
B
)
=
(
a
,
b
)
def
extension_~>
[
A
,
B
]
(
a
:
A
)
(
b
:
B
)
:
(
A
,
B
)
scala
>
"one"
~>
1
val
res0
:
(
String
,
Int
)
=
(
one
,
1
)
Use the @infix
annotation to allow infix operator notation, i.e., "one" ~> 1
. Use @alpha
to define an alphanumeric name in byte code for this method.
The syntax for defining an extension method. Any type parameters used by the methods that follow must go after the keyword extension
. (The whitespace is arbitrary.) Next we define the method ~>
, like we would if this were a “regular” type.
Note the signature the compiler reports for the generated method. It is named extension_~>
and it takes two parameter lists. The first one is for the target instance of type A
being extended. The second list is the same one specified when we declared the ~>
method. We’ll see this naming convention again for anonymous given instances below.
Now, when the compiler sees "one" ~> 1
, it will look for a corresponding extension_~>
that is in scope and type compatible in the left-hand and right-hand types. Our definition satisfies this requirement for all types. Then the compiler will emit code to call the extension method. No wrapping in a new instance is required. Hence, implicit conversion is eliminated.
Let’s complete an example we started in OperatorOverloading
, where we showed that parameterized types with two parameters can be written with infix notation, but at the time, we didn’t know how to support using the same type name as an operator for constructing instances. Specifically, we defined a type !!
allowing declarations like Int !! String
, but we couldn’t define a value of this type using the same literal syntax, for example, 2 !! "two"
. Now we can do this by defining an extension method !!
as follows:
// src/script/scala/progscala3/contexts/InfixTypeRevisited.scala
scala
>
import
scala.annotation.
{
alpha
,
infix
}
scala
>
@alpha
(
"BangBang"
)
case
class
!!
[
A
,
B
]
(
a
:
A
,
b
:
B
)
scala
>
extension
[
A
,
B
]
(
a
:
A
)
def
!!
(
b
:
B
)
:
A
!!
B
=
!!
(
a
,
b
)
def
extension_!!
[
A
,
B
]
(
a
:
A
)
(
b
:
B
)
:
A
!!
B
scala
>
val
ab1
:
Int
!!
String
=
1
!!
"one"
|
val
ab2
:
Int
!!
String
=
!!
(
1
,
"one"
)
val
ab1
:
Int
!!
String
=
!!
(
1
,
one
)
val
ab2
:
Int
!!
String
=
!!
(
1
,
one
)
The same case class defined in OperatorOverloading
.
The extension method definition. When only one method is defined, you can omit the colon (or curly braces) and even define it on the same line as shown.
This line failed to compile before, but now the extension method is applied to Int
and invoked with the String
argument "one"
.
When defining just one extension method for a type, the colon at the end of the opening line or curly braces can be omitted. For consistency and easier reading, consider always using the colon or braces.
Both of our examples did not need to add additional fields to the target type nor was there an interface that made sense to implement. When those are required, we’ll use type classes, discussed next.
There was a “context” for both extensions. Users only need ->
or !!
in certain, limited circumstances. These would not be good methods to add to the source code for all types! With extension methods, we get the best of both worlds, calling “methods” like ->
when we need them, while keeping types as focused as possible.
So far we have extended classes. What about extension methods on object
s? An object can be thought of as a singleton. To get its type, use Foo.type
:
scala
>
object
Foo
:
|
def
one:
Int
=
1
scala
>
extension
(
foo
:
Foo.
type
)
def
two
:
String
=
"two"
def
extension_two
(
foo
:
Foo.
type
)
:
String
scala
>
Foo
.
one
|
Foo
.
two
val
res0
:
Int
=
1
val
res1
:
String
=
two
The next step beyond extension methods is to add not only methods to types, but also state (fields) and to implement an abstraction, so all type extensions are done uniformly. A term that is popular for these kinds of extensions is type classes, which comes from the Haskell language, where this idea was pioneered. The word class in this context is not the same as Scala’s OOP concept of classes, which can be confusing.
As an example, suppose we have a collection of Shape
s from ASampleApplication
and we want the ability to call a toJSON
method on them that returns a JSON representation appropriate for each type? If we write someShape.toJSON
, We want the Scala compiler to invoke some mechanism that implements this functionality.
A type class defines an abstraction with optional state (fields) and behavior (methods). They provide a another way to implement mixin composition (TraitsInterfacesAndMixinsInScala
). The abstraction is valuable for ensuring that all “instances” of the type class follow the same protocol uniformly.
First, we need a trait
for the state and behavior we want to add. To keep things simple, we’ll return JSON-formatted strings, not objects from some JSON library (of which there are many…):
// src/main/scala/progscala3/contexts/json/ToJSON.scala
package
progscala3.contexts.json
trait
ToJSON
[
T
]
:
extension
(
t
:
T
)
def
toJSON
(
name
:
String
,
level
:
Int
)
:
String
protected
val
INDENTATION
=
" "
protected
def
indentation
(
level
:
Int
)
:
(
String
,
String
)
=
(
INDENTATION
*
level
,
INDENTATION
*
(
level
+
1
))
This is the Scala 3 type class pattern. We define a trait with a type parameter and we define extension methods. Although we don’t actually use the type parameter in the body of this particular type class, the parameter will be essential for disambiguating one type class instance, such as the one for Circle
, from another, such as the one for Rectangle
.
The public method users care about is toJSON
. The protected
method, indentation
, and immutable state, INDENTATION
, are implementation details.
The type class pattern solves the limitation discussed above for extension methods alone. We can define and implement an abstraction and we can add arbitrary state as fields.
Now we create instances for our Shape
s:
// src/main/scala/progscala3/contexts/typeclass/new1/ToJSONTypeClasses.scala
package
progscala3.contexts.typeclass.new1
import
progscala3.introscala.shapes.
{
Point
,
Shape
,
Circle
,
Rectangle
,
Triangle
}
import
progscala3.contexts.json.ToJSON
given
ToJSON
[
Point
]
:
extension
(
point
:
Point
)
def
toJSON
(
name
:
String
,
level
:
Int
)
:
String
=
val
(
outdent
,
indent
)
=
indentation
(
level
)
s"""
"
$name
"
: { |
${
indent
}
"
x
"
:
"
${
point
.
x
}
"
, |
${
indent
}
"
y
"
:
"
${
point
.
y
}
"
|
$outdent
}
"""
.
stripMargin
given
ToJSON
[
Circle
]
:
extension
(
circle
:
Circle
)
def
toJSON
(
name
:
String
,
level
:
Int
)
:
String
=
val
(
outdent
,
indent
)
=
indentation
(
level
)
s"""
"
$name
"
: { |
${
indent
}
${
circle
.
center
.
toJSON
(
"center"
,
level
+
1
)
}
, |
${
indent
}
"
radius
"
:
${
circle
.
radius
}
|
$outdent
}
"""
.
stripMargin
// And similarly for Rectangle and Triangle
@main
def
TryJSONTypeClasses
(
)
=
println
(
Circle
(
Point
(
1.0
,
2.0
)
,
1.0
)
.
toJSON
(
"circle"
,
0
)
)
println
(
Rectangle
(
Point
(
2.0
,
3.0
)
,
2
,
5
)
.
toJSON
(
"rectangle"
,
0
)
)
println
(
Triangle
(
Point
(
0.0
,
0.0
)
,
Point
(
2.0
,
0.0
)
,
Point
(
1.0
,
2.0
)
)
.
toJSON
(
"triangle"
,
0
)
)
The given
keyword declares a conversion for ToJSON[Point]
. The extension method for ToJSON
is implemented as required for point
A given for ToJSON[Circle]
.
Running TryJSONTypeClasses
prints the following:
>
runMain
progscala3
.
contexts
.
typeclass
.
new1
.
TryJSONTypeClasses
...
"circle"
:
{
"
center
"
:
{
"
x
"
:
"1
.
0"
,
"
y
"
:
"2
.
0"
},
"radius"
:
1
.
0
}
...
If you use braces, the colon on the given
line is replaced with an opening curly brace and a corresponding closing brace is required. The same applies for the definition of the anonymous ToJSON
subtype, of course.
There’s a flaw with our implementation, though. If we put those shapes in a sequence, shapes
, and try shapes.foreach(s ⇒ println(s.toJSON("shape", 0)))
, we get an error that Shape
doesn’t have a toJSON
method. Polymorphic dispatch doesn’t work here.
What if we add a given for Shape
that delegates to the others?
given
ToJSON
[
Shape
]
:
extension
(
shape:
Shape
)
def
toJSON
(
name:
String
,
level:
Int
)
:
String
=
shape
.
toJSON
(
name
,
level
)
Seems legit, but the compiler says we have an “infinite recursion”. Again, we aren’t actually calling a polymorphic method toJSON
defined in the old-fashioned way for the hierarchy. So, the call shape.toJSON(level)
attempts to call the extension method recursively.
What about pattern matching on the type of Shape
?
given
ToJSON
[
Shape
]
:
extension
(
shape:
Shape
)
def
toJSON
(
name:
String
,
level:
Int
)
:
String
=
shape
match
case
c
:
Circle
=>
c
.
toJSON
(
name
,
level
)
case
r
:
Rectangle
=>
r
.
toJSON
(
name
,
level
)
case
t
:
Triangle
=>
t
.
toJSON
(
name
,
level
)
We still get an infinite recursion at runtime, but the compiler can’t detect it! So, instead, let’s call the compiler generated toJSON
implementations directly. A synthesized object
is output for each specific given
:
// src/main/scala/progscala3/contexts/typeclass/new2/ToJSONTypeClasses.scala
...
given
ToJSON
[
Shape
]
:
extension
(
shape:
Shape
)
def
toJSON
(
name:
String
,
level:
Int
)
:
String
=
shape
match
case
c
:
Circle
=>
given_ToJSON_Circle
.
extension_toJSON
(
c
)(
name
,
level
)
case
r
:
Rectangle
=>
given_ToJSON_Rectangle
.
extension_toJSON
(
r
)(
name
,
level
)
case
t
:
Triangle
=>
given_ToJSON_Triangle
.
extension_toJSON
(
t
)(
name
,
level
)
...
@main
def
TryJSONTypeClasses
()
=
val
c
=
Circle
(
Point
(
1.0
,
2.0
),
1.0
)
val
r
=
Rectangle
(
Point
(
2.0
,
3.0
),
2
,
5
)
val
t
=
Triangle
(
Point
(
0.0
,
0.0
),
Point
(
2.0
,
0.0
),
Point
(
1.0
,
2.0
))
println
(
"==== Use shape.toJSON:"
)
Seq
(
c
,
r
,
t
).
foreach
(
s
=>
println
(
s
.
toJSON
(
"shape"
,
0
)))
println
(
"==== call toJSON on each shape explicitly:"
)
println
(
c
.
toJSON
(
"circle"
,
0
))
println
(
r
.
toJSON
(
"rectangle"
,
0
))
println
(
t
.
toJSON
(
"triangle"
,
0
))
The output of …typeclass.new2.TryJSONTypeClasses
(not shown) indicates that calling shape.toJSON
and circle.toJSON
(for example) now both work as desired, but we are relying on an obscure implementation detail that could change in a future release of the compiler. Note the naming conventions, given_…
and extension_…
, which we saw previously.
There is an easy fix. We can give names to the givens and then call them:
// src/main/scala/progscala3/contexts/typeclass/new3/ToJSONTypeClasses.scala
...
given
ToJSON
[
Shape
]
:
extension
(
shape:
Shape
)
def
toJSON
(
name:
String
,
level:
Int
)
:
String
=
shape
match
case
c
:
Circle
=>
circleToJSON
.
extension_toJSON
(
c
)(
name
,
level
)
case
r
:
Rectangle
=>
rectangleToJSON
.
extension_toJSON
(
r
)(
name
,
level
)
case
t
:
Triangle
=>
triangleToJSON
.
extension_toJSON
(
t
)(
name
,
level
)
given
circleToJSON
as
ToJSON
[
Circle
]
:
...
Note the as
keyword after the name. Instead of the synthesized name given_ToJSON_Circle
, it will be circleToJSON
, and similarly for the other shapes. We must still use the synthesized method name for the extension method, but at least we have more control over the object name.
When used as shown, as
is a “soft” reserved word. You can still use it as an identifier elsewhere. However, given
is always reserved.
At this point, if we still want to simulate polymorphic behavior, consider refactoring the code to move the implementations of the toJSON
methods for each concrete Shape to a regular helper object. Then call its methods instead of using the compiler generated objects. See …typeclass.new4.TryJSONTypeClasses
in the code examples for one approach.
Keep the given instances as concise as possible. Consider moving some of the code to “regular” types that operate as helpers.
Finally, note that Point
and the concrete subtypes of Shape
are not related in the type hierarchy (well, except for AnyRef
way at the top). Hence, this extension mechanism is ad hoc polymorphism, because the polymorphic behavior of toJSON
is not tied to the type system, as it would be in subtype polymorphism. Subtype polymorphism is nice for allowing parent types to declare behaviors that can can be defined in subtypes. We had to hack around this missing feature for toJSON
! For completeness, recall that we discussed a third kind of polymorphism, parametric polymorphism, in AbstractTypesVsParameterizedTypes
, where containers like Container[A]
behave uniformly for any type A
.
Sometimes a type class will define members that make more sense as the analogs of companion object members, rather than instance members. To see this, let’s look at type classes for Semigroup and Monoid. Semigroup generalizes the notion of addition or composition. You know how addition works for numbers, and even strings can be “added”. Monoid adds the idea of a “unit” value. If you add zero to a number, you get the number back. If you add a string to an empty string, you get the first string back.
Here are the definitions for these types:2
// src/script/scala/progscala3/contexts/MonoidTypeClass.scala
trait
Semigroup
[
T
]
:
extension
(
t
:
T
)
:
def
combine
(
other
:
T
)
:
T
def
<+>
(
other
:
T
)
:
T
=
t
.
combine
(
other
)
trait
Monoid
[
T
]
extends
Semigroup
[
T
]
:
def
unit
:
T
given
StringMonoid
as
Monoid
[
String
]
:
def
unit
:
String
=
""
extension
(
s
:
String
)
def
combine
(
other
:
String
)
:
String
=
s
+
other
given
IntMonoid
as
Monoid
[
Int
]
:
def
unit
:
Int
=
0
extension
(
i
:
Int
)
def
combine
(
other
:
Int
)
:
Int
=
i
+
other
Define an instance extension method combine
and an alternative operator methods <+>
that calls combine
. Note that combining one element with another of the same type returns a new element of the same type, like adding numbers. For given instances of the type class, we will only need to define combine
.
The definition for unit
, i.e., zero for addition of numbers. It’s not defined as an extension method, but an object method, because we only need one instance of the value for all T
.
Instances of the type class for String
and Int
. Note how unit
and combine
are defined.
The Monoid combine operation is associative, so here are examples of both instances in action.
"2"
<+>
(
"3"
<+>
"4"
)
// "234"
(
"2"
<+>
"3"
)
<+>
"4"
// "234"
StringMonoid
.
unit
<+>
"2"
// "2"
"2"
<+>
StringMonoid
.
unit
// "2"
2
<+>
(
3
<+>
4
)
// 9
(
2
<+>
3
)
<+>
4
// 9
IntMonoid
.
unit
<+>
2
// 2
2
<+>
IntMonoid
.
unit
// 2
Notice how each unit
is referenced. This is why we gave these given instances explicit names, so it’s easier to remember what to call them, instead of the default given_Monoid_String
, etc.
Finally, we don’t actually need to define separate instances for each numeric type. Here is how to implement it once for a T
for which Numeric[T]
exists:
given
NumericMonoid
[
T
](
using
num
:
Numeric
[
T
])
as
Monoid
[
T
]
:
def
unit:
T
=
num
.
zero
extension
(
t
:
T
)
def
combine
(
other
:
T
)
:
T
=
num
.
plus
(
t
,
other
)
2.2
<+>
(
3.3
<+>
4.4
)
// 9.9
(
2.2
<+>
3.3
)
<+>
4.4
// 9.9
BigDecimal
(
3.14
)
<+>
NumericMonoid
.
unit
NumericMonoid
[
BigDecimal
].
unit
<+>
BigDecimal
(
3.14
)
//tag::numericdefinition[]
When accessing NumericMonoid[BigDecimal].unit
, the type parameter could be inferred when it was used as the argument for the extension method <+>
. However, when it was used as the instance to be extended with <+>
, the type parameter was required.
To implement the same type class and instances in Scala 2 sytanx (which is still allowed), you write an implicit conversion that wraps the Point
and Shape
instances in a new instance of a type that has the toJSON
method, then call the method.
First, we need a slightly different ToJSON
trait, because the extension method code in ToJSON
won’t work with Scala 2:
// src/main/scala/progscala3/contexts/typeclass/old/ToJSONOldTypeClasses.scala
package
progscala3.contexts.typeclass.old
import
scala.language.implicitConversions
trait
ToJSONOld
[
T
]
:
def
toJSON
(
level
:
Int
)
:
String
protected
val
INDENTATION
=
" "
protected
def
indentation
(
level
:
Int
)
:
(
String
,
String
)
=
(
INDENTATION
*
level
,
INDENTATION
*
(
level
+
1
)
)
We must enable implicit conversions.
Now this is a regular method. In the previous ToJSON
implementation, it was an extension method.
Indiscriminate use of implicit conversions can be confusing for code comprehension and it sometimes leads to unexpected behavior. Therefore, implicit conversions are treated as an optional feature by Scala. This means you must enable the feature explicitly with the import statement used for implicitConversions
or use the global -language:
implicitConversions
compiler flag.
Now here is an implementation of a toJSON
type class instances for Scala 2, which also works in Scala 3. We’ll only show the implementations for Point
and Circle
:
implicit
final
class
PointToJSON
(
point
:
Point
)
extends
ToJSONOld
[
Point
]
:
def
toJSON
(
name
:
String
,
level
:
Int
)
:
String
=
val
(
outdent
,
indent
)
=
indentation
(
level
)
s"""
"
$name
"
: { |
${
indent
}
"
x
"
:
"
${
point
.
x
}
"
, |
${
indent
}
"
y
"
:
"
${
point
.
y
}
"
|
$outdent
}
"""
.
stripMargin
implicit
final
class
CircleToJSON
(
circle
:
Circle
)
extends
ToJSONOld
[
Circle
]
:
def
toJSON
(
name
:
String
,
level
:
Int
)
:
String
=
val
(
outdent
,
indent
)
=
indentation
(
level
)
s"""
"
$name
"
: { |
${
indent
}
${
circle
.
center
.
toJSON
(
"center"
,
level
+
1
)
}
, |
${
indent
}
"
radius
"
:
${
circle
.
radius
}
|
$outdent
}
"""
.
stripMargin
.
.
.
@main
def
TryJSONOldTypeClasses
(
)
=
val
c
=
Circle
(
Point
(
1.0
,
2.0
)
,
1.0
)
val
r
=
Rectangle
(
Point
(
2.0
,
3.0
)
,
2
,
5
)
val
t
=
Triangle
(
Point
(
0.0
,
0.0
)
,
Point
(
2.0
,
0.0
)
,
Point
(
1.0
,
2.0
)
)
println
(
c
.
toJSON
(
"circle"
,
0
)
)
println
(
r
.
toJSON
(
"rectangle"
,
0
)
)
println
(
t
.
toJSON
(
"triangle"
,
0
)
)
The type class “instance” that implements ToJSONOld.toJSON
for Point
. It is called a type class instance following Haskell conventions, but we actually declare a class for it, which can be confusing.
The type class instance that implements toJSON
for Circle
.
Because these classes are declared as implicit, when the compiler sees circle.toJSON()
, for example, it will look for an implicit conversion in scope that returns some wrapper type that has this method.
The output of TryJSONOldTypeClasses
works as expected. However, we didn’t solve the problem of iterating through some Shape
s and calling toJSON
polymorphically. You can try that yourself.
We didn’t declare our implicit classes as cases classes. In fact, Scala doesn’t allow an implicit class to also be a case class. It wouldn’t make much sense anyway, because the extra, auto-generated code for the case class would never be used. Implicit classes have a very narrow purpose. Similarly, declaring them final
is recommended to eliminate some potential surprises when the compiler resolves which type classes to use.
If you need to support Scala 2 code for a while, then using the original type class pattern will work for a few versions of Scala 3. However, in most cases, it will be better to migrate to the new type class syntax, because it is more concise and purpose-built, and it doesn’t require implicit conversions.
We saw that an implicit conversion called ArrowAssoc
was used in the Scala 2 library to implement the "one" -> 1
idiom, whereas we could use an extension method in Scala 3. We also saw implicit conversions used for type classes in Scala 2, while Scala 3 combines extension methods and givens to avoid doing conversions.
Hence, in Scala 3, the need to do implicit conversions is greatly reduced, but it hasn’t disappeared completely. Sometimes you will want to convert between types for other reasons. Consider the following example that defines types to represent Dollars
, Percentage
s, and a person’s Salary
, where the gross salary and the percentage to deduct for taxes are encapsulated. When constructing a Salary
instance, we want to allow users to enter Double
s, for convenience:
// src/main/scala/progscala3/contexts/NewImplicitConversions.scala
package
progscala3.contexts
import
scala.language.implicitConversions
case
class
Dollars
(
amount
:
Double
)
:
override
def
toString
=
f"$$
$amount
%.2f"
case
class
Percentage
(
amount
:
Double
)
:
override
def
toString
=
f"
${
(
amount
*
100.0
)
}
%.2f%%"
case
class
Salary
(
gross
:
Dollars
,
taxes
:
Percentage
)
:
def
net:
Dollars
=
Dollars
(
gross
.
amount
*
(
1.0
-
taxes
.
amount
))
Note that we import scala.language.implicitConversions
. The Dollars
class encapsulates a Double
for the amount, with toString
overridden to return the familiar “$dollars.cents” output. Similarly, Percentage
wraps a Double
and overrides toString
.
Let’s try it:
@main
def
TryImplicitConversions
(
)
=
given
Conversion
[
Double
,
Dollars
]
=
d
=>
Dollars
(
d
)
given
Conversion
[
Double
,
Percentage
]
=
d
=>
Percentage
(
d
)
val
salary
=
Salary
(
100
_000
.
0
,
0.20
)
println
(
s"
salary:
$salary
. Net pay:
${
salary
.
net
}
"
)
The syntax for declaring a given conversion from Double
to Dollars
and a second conversion from Double
to Percentage
.
Running this example prints the following:
salary
:
Salary
(
$100000.
00
,
20
.
00
%
).
Net
pay
:
$80000.
00
The declaration of the Conversion[Double,Dollars]
given is shorthand for the following longer form:
given
Conversion
[
Double
,Dollars
]
:
def
apply
(
d:
Double
)
:
Dollars
=
Dollars
(
d
)
By the way, if you define the given for converting Double
s to Dollars
in the REPL, observe what happens:
...
scala
>
given
Conversion
[
Double
,Dollars
]
=
d
=>
Dollars
(
d
)
def
given_Conversion_Double_Dollars
:
Conversion
[
Double
,Dollars
]
scala
>
given
dd
as
Conversion
[
Double
,Dollars
]
=
d
=>
Dollars
(
d
)
def
dd
:
Conversion
[
Double
,Dollars
]
In our type classes above, object
s were generated. Here, methods are generated. As we saw previously, for an anonymous given, the generated name follows the convention given_…
.
Scala 3 still supports the Scala 2 mechanism of using an implicit method for conversion:
implicit
def
toDollars
(
d
:
Double
)
:
Dollars
=
Dollars
(
d
)
Here is a summary of the lookup rules used by the compiler to find and apply conversions conversions. I’ll use given
and given instances to refer to both new and old style conversions:
No conversion will be attempted if the object and method combination type check successfully.
Only given instances for conversion are considered.
Only given instances in the current scope are considered, as well as givens defined in the companion object of the target type.
Given conversions aren’t chained to get from the available type, through intermediate types, to the target type. Only one conversion will be considered.
No conversion is attempted if more than one possible conversion could be applied and have the same scope. There must be one and only one, unambiguous possibility.
TODO: Verify which mixins support derivation at the time Scala 3 is released. Also verify maturity of the implementation For example, derives Eql
shouldn’t be necessary for enum
and case class definitions, but it appears to be necessary. What is the final Scala 3 implementation?
Type class derivation is the idea that we should be able to automatically generate type class given
instances as long as they obey a minimum set of requirements. A class uses the new keyword derives
, which works like extends
or with
, to trigger derivation.
For example, Scala 3 introduces scala.Eql
, which restricts use of the comparison operators ==
and !=
for instances of arbitrary types. Normally, it’s allowed to do these comparisons, but when the compiler flag -language:strictEquality
or the import statement import scala.language.strictEquality
is used, then the comparison operators are only allowed in certain specific contexts. Here is an example:
// src/main/scala/progscala3/contexts/Derivation.scala
package
progscala3.contexts
import
scala.language.strictEquality
enum
Tree
[
T
]
derives
Eql
:
case
Branch
(
left:
Tree
[
T
],
right
:
Tree
[
T
])
case
Leaf
(
elem
:
T
)
@main
def
TryDerived
()
=
import
Tree._
val
l1
=
Leaf
(
"l1"
)
val
l2
=
Leaf
(
2
)
val
b
=
Branch
(
l1
,
Branch
(
Leaf
(
"b1"
),
Leaf
(
"b2"
)))
assert
(
l1
==
l1
)
// assert(l1 != l2) // Compilation error!
assert
(
l1
!=
b
)
assert
(
b
==
b
)
println
(
s"For String, String:
${
implicitly
[
Eql
[
Tree
[
String
]
,Tree
[
String
]]]
}
"
)
println
(
s"For Int, Int:
${
implicitly
[
Eql
[
Tree
[
Int
]
,Tree
[
Int
]]]
}
"
)
// Compilation error:
// println(s"For String, Int: ${implicitly[Eql[Tree[String],Tree[Int]]]}")
Because of the derives Eql
clause in the Tree
declaration, the equality checks in the assertions are allowed. The derives Eql
clause has the effect of generating the following given instance:
given
Eql
[
Tree
[
T
]
,Tree
[
T
]]
=
Eql
.
derived
Eql.derived
is the following:
object
Eql
:
object
derived
extends
Eql
[
Any
,Any
]
...
Furthermore, T
will be constrained to types with given Eql[T, T] = Eql.derived
. What all this effectively means that we can only compare Tree[T]
instances for the same T
types.
The terminology used is Tree
is the deriving type and the Eql
instance is a derived instance.
In general, any type T
defined with a companion object that has the derived
instance or method can be used with derives T
clauses. We’ll discuss how methods are implemented in TypeClassDerivationImplementationDetails
, after we have learned the metaprogramming details required. The reason Eql
and the strictEquality
language feature were introduced is discussed in MultiversalEquality
.
If you want to enforce stricter use of comparison operators, use -language:strictEquality
, but expect to add derives Eql
to many of your types.
In ATasteOfFutures
we imported an implicit ExecutionContext
, scala.concurrent.ExecutionContext.Implicits.global
. The name of the enclosing object Implicits
reflects a common convention in Scala 2 for making implicit definitions more explicit in code that uses them, at least if you pay attention to the import statements.
Scala 3 introduces a new way to control imports of givens and implicits, which provides an effective alternative form of visibility, as well as allowing developers to use wild-card imports frequently while restricting if and when givens and implicits are also imported.
Consider the following example adapted from the Dotty documentation:
// src/script/scala/progscala3/contexts/GivenImports.scala
object
O1
:
val
name
=
"O1"
val
m
(
s
:
String
)
=
s"
$s
, hello from
$name
"
class
C1
given
c1
as
C1
class
C2
given
c2
as
C2
Now consider these import statements:
import
O1._
// Imports everything EXCEPT the givens, c1 and c2
import
O1.
{
given
_
}
// Imports ONLY the givens, c1 and c2
import
O1.
{
given
c1
}
// Imports just c1 explicitly
import
O1.
{
given
_
,
_
}
// Imports everything in O1
A given
import selector also brings old style implicits into scope.
What if the given
instances are anonymous and you don’t want to use the wild card?
object
O2
:
class
C1
given
C1
class
C2
given
C2
given
intOrd
as
Ordering
[
Int
]
given
listOrd
[
T:
Ordering
]
as
Ordering
[
List
[
T
]]
You can import by type. Note the ?
wild card for the type parameter, which means both Ordering
givens will be imported:
import
O2.
{
given
C1
,
given
Ordering
[
?
]}
Because this is a breaking change in how _
wild cards work for imports, it is being implemented gradually:
In Scala 3.0 an old-style implicit definition can be brought into scope either by a _
or a given _
wildcard selector.
In Scala 3.1 an old-style implicit accessed through a _
wildcard import will give a deprecation warning.
In some version after 3.1, old-style implicits accessed through a _
wildcard import will give a compiler error.
Finally, the older Scala 2 implicit conversions are still allowed, where an implicit def
is used, for example:
implicit
def
doubleToDollars
(
d
:
Double
)
:
Dollars
=
Dollars
(
d
)
Unlike the Scala 2 alternative to extension methods, we don’t need an implicit class here, like ArrowAssoc
above, because Dollars
has all the methods we need. This method would be invoked to do the conversion exactly the same way as the given Conversion[Double,Dollars]
above.
Extension methods and given
definitions obey the same scoping rules as other declarations, i.e., they must be visible to be considered. The previous examples scoped the extension methods to packages, such as the new1
, new2
, etc. packages. They were not visible unless the package contents were imported or we were already in the scope of that package.
Within a particular scope, there could be several candidate givens or extension methods that the compiler might use for a type extension. The Dotty documentation has the details for Scala 3’s resolution rules. I’ll summarize the key points here. Givens are also used to resolve implicit parameters in method using clauses, which we’ll explore in the next chapter. The same resolution rules apply.
I’ll use the term “given” in the following discussion to include given instances, extension methods, and Scala 2 implicit methods, values, and classes, depending on the scenario. Resolving to a particular given happens in the following order:
The compiler always puts some library givens in scope, while other library givens require an import statement. For example, Predef
extends a type called LowPriorityImplicits
, which makes the givens defined in Predef
lower priority when potential conflicts arise with other givens in scope. The rational is that the other givens are likely to be user defined or imported from special libraries, and hence more “important” to the user.
Let’s look at a final example of extension, one that lets us define our own string interpolation capability. Recall from InterpolatedStrings
that Scala has several built-in ways to format strings through interpolation. For example:
val
(
first
,
last
)
=
(
"Buck"
,
"Trends"
)
println
(
s"Hello,
${
first
}
${
last
}
"
)
There are also StringContext
methods named f
and raw
, where f
supports printf
format directives and raw
doesn’t interpret escape characters:
scala
>
val
pi
=
3.14159
scala
>
f"
$pi
%5.3f or
${
pi
}
%7.5f"
val
res0
:
String
=
3.142
or
3.14159
scala
>
raw" $pi $pi again"
val
res1
:
String
=
t
3.14159
n
3.14159
again
We’ll look at a simplistic implementation of a SQL query compiler named sql
. When the compiler sees an expression like sql"SELECT $column FROM $table;"
, it will be translated to the following:
StringContext
(
"SELECT "
,
"FROM "
,
";"
).
sql
(
column
,
table
)
Note how the embedded expressions become arguments to sql
, while the other string tokens are arguments to StringContext.apply
. However, scala.StringContext
doesn’t have a sql
method, so an implicit conversion to another type or an extension method is required.
Let’s define sql
for StringContext
. For simplicity, it will only handle SQL queries of the form sql"SELECT columns FROM table;"
with the columns and table strings specified as part of the string or using embedded expressions. The extracted column and table names are returned in an instance of a simple case class SQLQuery
. It would be possible to use the same approach with a real SQL parser and return a query object for a library like JDBC. I won’t show the whole implementation (which is somewhat of a “hack” for this simple case), but just the declarations:
// src/main/scala/progscala3/contexts/SQLStringInterpolator.scala
package
progscala3.contexts
object
SimpleSQL
:
case
class
SQLQuery
(
columns:
Seq
[
String
]
=
Nil
,
table
:
String
=
""
)
extension
(
sc
:
StringContext
)
:
def
sql
(
values:
String*
)
:
SQLQuery
=
// Extract the column names and table name.
SQLQuery
(
columns
,
table
)
See the SQLStringInterpolator
source code for the full details. Here is how to use it:
scala
>
import
progscala3.contexts.SimpleSQL._
scala
>
val
query1
=
sql
"SELECT one, two, three FROM t1;"
val
query1
:
...SimpleSQL.SQLQuery
=
SQLQuery
(
Vector
(
one
,
two
,
three
),
t1
)
scala
>
val
cols
=
Seq
(
"four"
,
"five"
).
mkString
(
", "
)
|
val
table
=
"t2"
|
val
query2
=
sql
"SELECT $cols FROM $table;"
val
cols
:
Seq
[
String
]
=
List
(
four
,
five
)
val
table
:
String
=
t2
val
query2
:
...SQLQuery
=
SQLQuery
(
Vector
(
four
,
five
),
t2
)
As shown, custom string interpolators can return any type you want, not just a new String
, like s
, f
, and raw
return. Hence, they can function as instance factories that are driven by data encapsulated in strings.
Let’s step back for a moment and ponder what we just accomplished in the previous example. We added new functionality to an existing library type without editing the source code for it!
This desire to extend modules without modifying their source code is called the Expression Problem, a term coined by Philip Wadler.
Object-oriented programming solves this problem with subtyping, more precisely called subtype polymorphism. We program to abstractions and use derived classes when we need changed behavior. Bertrand Meyer coined the term Open/Closed Principle to describe a principled OOP approach, where base types declare the behaviors as abstract that should be open for extension or variation in subtypes, while keeping invariant behaviors closed to modification. The base types are never modified for extension.
Scala certainly supports this technique, but it has drawbacks. What if it’s questionable that we should have that behavior defined in the type hierarchy in the first place? What if the behavior is only needed in a few contexts, while for most contexts, it’s just a burden that the code carries around?
It can be a burden for several reasons. First, the extra, mostly-unused code is a maintenance burden. Developers have to understand it, even when working on other aspects of the code, so they don’t break it inadvertently. Second, it’s also inevitable that most defined behaviors will be refined over time. Every change to a feature that some clients aren’t using forces unwanted updates on client code.
This problem led to the Single Responsibility Principle, a classic design principle that encourages us to define abstractions and implement types with just a single behavior.
Still, in realistic scenarios, it’s sometimes necessary for an object to combine several behaviors. For example, a service often needs to “mix in” the ability to log messages. Scala makes these mixin features relatively easy to implement, as we saw in TraitsInterfacesAndMixinsInScala
. We can even declare instances that mix in traits without first defining a class.
In general, mixins, extension methods, and type classes provide robust and principled solutions to the Open/Closed Principle while allowing the core implementations of types to obey the Single Responsibility Principle.
Why not take an extreme approach and define types with very little state and behavior (sometimes called anemic types), then add most behaviors using mixin traits, type classes, extension methods, even implicit conversions?
First, when using a type class or implicit conversion, the resolution algorithm requires more work by the compiler than just finding the logic inside the type’s original definition. Also, there can be more boilerplate writing extensions compared to the alternative of defining constructs inside the type. Therefore, a project that over-uses these tools is a project that is slow to build, as well as potentially hard to comprehend when reading the code.
Another problem for the extension mechanisms explored in this chapter is that you effectively lose several benefits of object orientation!
First, code evolution can be challenging if the extensions depend on details of the types they extend, like our ToJSON
example in the last chapter. The details have to be coordinated in more than one place, the locations of the extensions, as well as the type definitions themselves. Fortunately, coupling between type definitions and extensions are limited to the public interfaces, as an extension has no access to private or protected members of a type.
A second issue is the loss of object-oriented method dispatch. We had to do some hacking to support Shape.toJSON
in a polymorphic way.
If instead, toJSON
were declared abstract in Shape
and implemented in Circle
, Rectangle
, etc., then this code would work with the usual object-oriented dispatch rules.
Most of the time, the core domain logic belongs in the type definition. Ancillary behaviors, like serializing to JSON and logging, belong in mixins or type classes. However, if your applications use toJSON
frequently for your domain classes, it might be a good idea to move this behavior into the type definitions, on balance.
When should use use type classes and extension methods vs. mix-in composition? For example, recall the Logging
trait example we saw in TraitsInterfacesAndMixinsInScala
. If the trait has orthogonal state and behavior, like logging, that can be mixed into many different objects, then a mixin trait is often best. If the behavior has to be defined carefully for each type, like toJSON
, then type classes are best.
We started our exploration of context abstractions in Scala 2 and 3, beginning with tools to extend types with additional state and behavior, such as type classes, extension methods, and implicit conversions.
Part 2 explores using clauses, which work with given instances to address particular design scenarios and to simplify user code.
1 Adapted from this Dotty documentation.
2 Adapted from https://dotty.epfl.ch/docs/reference/contextual/type-classes.html.
3.19.234.150