5.1 Case Classes and Sealed Traits | 81 |
5.2 Pattern Matching | 83 |
5.3 By-Name Parameters | 89 |
5.4 Implicit Parameters | 91 |
5.5 Typeclass Inference | 94 |
@ def
getDayMonthYear(
s:
String
)
=
s match
{
case
s"$day-$month-$year" =>
println(
s"found day: $day, month: $month, year: $year")
case
_ =>
println(
"not a date"
)
}
@ getDayMonthYear(
"9-8-1965"
)
found day:
9
,
month:
8
,
year:
1965
@ getDayMonthYear(
"9-8"
)
not a date
</> 5.1.scala
Snippet 5.1: using Scala's pattern matching feature to parse simple string patterns
This chapter will cover some of the more interesting and unusual features of Scala. For each such feature, we will cover both what the feature does as well as some common use cases to give you an intuition for what it is useful for.
Not every feature in this chapter will be something you use day-to-day. Nevertheless, even these less-commonly-used features are used often enough that it is valuable to have a high-level understanding for when you eventually encounter them in the wild.
es are like normal case
class
es, but meant to represent class
es which
are "just data": where all the data is immutable and public, without any mutable
state or encapsulation. Their use case is similar to "structs" in C/C++, "POJOs"
in Java or "Data Classes" in Python or Kotlin. Their name comes from the fact
that they support pattern matching (5.2) via the class
keyword.case
Case classes are defined with the
keyword, and can be instantiated
without case
. All of their constructor parameters are public fields by default.new
|
|
s give you a few things for free:case
class
A
implemented to show you the constructor parameter values.
toString
A
implemented to check if the constructor parameter values are equal==
A
method to conveniently create modified copies of the case class
instance.
copy
|
|
Like normal classes, you can define instance methods or properties in the body
of the
:case
class
|
|
es are a good replacement for large tuples, since instead of
extracting their values via case
class
.
_1.
_2
you can extract the values via
their names like .
_7
and .
x
. That is much easier than trying to remember
exactly what field .
y
in a large tuple represents!.
_7
s can also be defined trait
, and only extended by a fixed set of sealed
es. In the following example, we define a case
class
extended by
two sealed
trait
Point
es: case
class
Point2D
and Point3D
:
@ {
sealed
trait
Point
case
class
Point2D(
x:
Double
,
y:
Double
)
extends
Point
case
class
Point3D(
x:
Double
,
y:
Double
,
z:
Double
)
extends
Point
}
@ def
hypotenuse(
p:
Point)
=
p match
{
case
Point2D(
x,
y)
=>
math.
sqrt(
x *
x +
y *
y)
case
Point3D(
x,
y,
z)
=>
math.
sqrt(
x *
x +
y *
y +
z *
z)
}
@ val
points:
Array[
Point]
=
Array(
Point2D(
1
,
2
)
,
Point3D(
4
,
5
,
6
)
)
@ for
(
p <-
points)
println(
hypotenuse(
p)
)
2.23606797749979
8.774964387392123
</> 5.8.scala
The core difference between normal
s and trait
s can be
summarized as follows:sealed
trait
Normal
s are open, so any number of classes can inherit from the
trait as long as they provide all the required methods, and instances of those
classes can be used interchangeably via the trait
's required methods.trait
s are closed: they only allow a fixed set of classes to
inherit from them, and all inheriting classes must be defined together with
the trait itself in the same file or REPL command (hence the curlies sealed
trait
surrounding the {
}
Point
/Point2D
/Point3D
definitions above).
Because there are only a fixed number of classes inheriting from
, we can use pattern matching in the sealed
trait
Point
function above to
define how each kind of def
hypotenusePoint
should be handled.
Both normal
s and trait
s are common in Scala applications:
normal sealed
trait
s for interfaces which may have any number of subclasses, and
trait
s where the number of subclasses is fixed.sealed
trait
Normal
s and trait
s make different things easy:sealed
trait
A normal
hierarchy makes it easy to add additional sub-classes: just
define your class and implement the necessary methods. However, it makes it
difficult to add new methods: a new method needs to be added to all existing
subclasses, of which there may be many.trait
A
hierarchy is the opposite: it is easy to add new methods,
since a new method can simply pattern match on each sub-class and decide what
it wants to do for each. However, adding new sub-classes is difficult, as you
need to go to all existing pattern matches and add the sealed
trait
to handle your
new sub-classcase
In general,
s are good for modelling hierarchies where you expect
the number of sub-classes to change very little or not-at-all. A good example of
something that can be modeled using sealed
trait
is JSON:sealed
trait
@ {
sealed
trait
Json
case
class
Null(
)
extends
Json
case
class
Bool(
value:
Boolean
)
extends
Json
case
class
Str(
value:
String
)
extends
Json
case
class
Num(
value:
Double
)
extends
Json
case
class
Arr(
value:
Seq[
Json]
)
extends
Json
case
class
Dict(
value:
Map[
String
,
Json]
)
extends
Json
}
</> 5.9.scala
A JSON value can only be JSON null, boolean, number, string, array, or dictionary.
JSON has not changed in 20 years, so it is unlikely that anyone will need to
extend our JSON
with additional subclasses.trait
While the set of sub-classes is fixed, the range of operations we may want to do on a JSON blob is unbounded: parse it, serialize it, pretty-print it, minify it, sanitize it, etc.
Thus it makes sense to model a JSON data structure as a closed
hierarchy rather than a normal open sealed
trait
hierarchy.trait
Scala allows pattern matching on values using the
keyword. This is
similar to the match
switch
statement found in other programming languages, but more
flexible: apart from
ing on primitive integers and strings, you can also
use match
to extract values from ("destructure") composite data types like
tuples and match
es. Note that in many examples below, there is a case
class
clause which defines the default case if none of the earlier cases matched.case
_ =>
5.2.1.1 Matching on Int | 5.2.1.2 Matching on String |
5.2.1.3 Matching on tuple ( Int , Int )
| 5.2.1.4 Matching on tuple ( Boolean , Boolean )
|
5.2.1.5 Matching on Case Classes:
| 5.2.1.6 Matching on String Patterns:
(Note that pattern matching on string patterns only supports simple glob-like
patterns, and doesn't support richer patterns like Regular Expressions. For
those, you can use the functionality of the |
Patterns can also be nested, e.g. this example matches a string pattern within a
pattern:case
class
@ case
class
Person(
name:
String
,
title:
String
)
@ def
greet(
p:
Person)
=
p match
{
case
Person(
s"$firstName $lastName",
title)
=>
println(
s"Hello $title $lastName")
case
Person(
name,
title)
=>
println(
s"Hello $title $name")
}
@ greet(
Person(
"Haoyi Li"
,
"Mr"
)
)
Hello Mr Li
@ greet(
Person(
"Who?"
,
"Dr"
)
)
Hello Dr Who?
</> 5.16.scala
Patterns can be nested arbitrarily deeply. The following example matches string
patterns, inside a
, inside a tuple:case
class
@ def
greet2(
husband:
Person,
wife:
Person)
=
(
husband,
wife)
match
{
case
(
Person(
s"$first1 $last1",
_)
,
Person(
s"$first2 $last2",
_)
)
if
last1 ==
last2 =>
println(
s"Hello Mr and Ms $last1")
case
(
Person(
name1,
_)
,
Person(
name2,
_)
)
=>
println(
s"Hello $name1 and $name2")
}
@ greet2(
Person(
"James Bond"
,
"Mr"
)
,
Person(
"Jane Bond"
,
"Ms"
)
)
Hello Mr and Ms Bond
@ greet2(
Person(
"James Bond"
,
"Mr"
)
,
Person(
"Jane"
,
"Ms"
)
)
Hello James Bond and Jane
</> 5.17.scala
The last two places you an use pattern matching are inside
-loops and for
definitions. Pattern matching in val
-loops is useful when you need to iterate
over collections of tuples:for
@ val
a =
Array[
(
Int
,
String
)
]
(
(
1
,
"one"
)
,
(
2
,
"two"
)
,
(
3
,
"three"
)
)
@ for
(
(
i,
s)
<-
a)
println(
s +
i)
one1
two2
three3
</> 5.18.scala
Pattern matching in
statements is useful when you are sure the value will
match the given pattern, and all you want to do is extract the parts you want.
If the value doesn't match, this fails with an exception:val
|
|
Pattern matching lets you elegantly work with structured data comprising case classes and sealed traits. For example, let's consider a simple sealed trait that represents arithmetic expressions:
@ {
sealed
trait
Expr
case
class
BinOp(
left:
Expr,
op:
String
,
right:
Expr)
extends
Expr
case
class
Literal(
value:
Int
)
extends
Expr
case
class
Variable(
name:
String
)
extends
Expr
}
</> 5.21.scala
Where BinOp
stands for "Binary Operation". This can represent the arithmetic
expressions, such as the following
|
|
|
|
|
|
For now, we will ignore the parsing process that turns the string on the left
into the structured
structure on the right: we will cover that in
Chapter 19: Parsing Structured Text. Let us instead consider two things you
may want to do once you have parsed such an arithmetic expression to the case
class
es we see above: we may want to print it to a human-friendly string, or we
may want to evaluate it given some variable values.case
class
Converting the expressions to a string can be done using the following approach:
Expr
is a Literal
, the string is the value of the literalExpr
is a Variable
, the string is the name of the variableExpr
is a BinOp
, the string is the stringified left expression,
followed by the operation, followed by the stringified right expressionConverted to pattern matching code, this can be written as follows:
@ def
stringify(
expr:
Expr)
:
String
=
expr match
{
case
BinOp(
left,
op,
right)
=>
s"(${
stringify(
left)
}
$op ${
stringify(
right)
}
)"
case
Literal(
value)
=>
value.
toString
case
Variable(
name)
=>
name
}
</> 5.24.scala
We can construct some of Expr
s we saw earlier and feed them into our
stringify
function to see the output:
|
|
Evaluation is a bit more complex than stringifying the expressions, but only
slightly. We need to pass in a values
map that holds the numeric value of
every variable, and we need to treat
, +
, and -
operations differently:*
@ def
evaluate(
expr:
Expr,
values:
Map[
String
,
Int
]
)
:
Int
=
expr match
{
case
BinOp(
left,
"+"
,
right)
=>
evaluate(
left,
values)
+
evaluate(
right,
values)
case
BinOp(
left,
"-"
,
right)
=>
evaluate(
left,
values)
-
evaluate(
right,
values)
case
BinOp(
left,
"*"
,
right)
=>
evaluate(
left,
values)
*
evaluate(
right,
values)
case
Literal(
value)
=>
value
case
Variable(
name)
=>
values(
name)
}
@ evaluate(
smallExpr,
Map(
"x"
->
10
)
)
res56:
Int
=
11
@ evaluate(
largeExpr,
Map(
"x"
->
10
,
"y"
->
20
)
)
res57:
Int
=
209
</> 5.27.scala
Overall, this looks relatively similar to the stringify
function we wrote
earlier: a recursive function that pattern matches on the expr
parameter
to handle each :
Expr
that implements case
class
Expr
. The cases handling
child-free Literal
and Variable
are trivial, while in the BinOp
case we
recurse on both left and right children before combining the result. This is a
common way of working with recursive data structures in any language, and
Scala's
s, sealed
trait
es and pattern matching make it concise and
easy.case
class
This Expr
structure and the printer and evaluator we have written are
intentionally simplistic, just to give us a chance to see how pattern matching
can be used to easily work with structured data modeled as
es and
case
class
s. We will be exploring these techniques much more deeply in
Chapter 20: Implementing a Programming Language.sealed
trait
@ def
func(
arg:
=>
String
)
=
.
.
.
Scala also supports "by-name" method parameters using a
syntax, which
are evaluated each time they are referenced in the method body. This has three
primary use cases::
=>
T
The following log
method uses a by-name parameter to avoid evaluating the
msg
unless it is actually going to get printed. This can help avoid
spending CPU time constructing log messages (here via :
=>
String
) even when logging is disabled:"Hello "
+
123
+
" World"
|
|
Often a method does not end up using all of its arguments all the time. In the above example, by not computing log messages when they are not needed, we can save a significant amount of CPU time and object allocations which may make a difference in performance-sensitive applications.
The getOrElse
and getOrElseUpdate
methods we saw in Chapter 4: Scala Collections are similar: these methods do not use the argument representing the
default value if the value we are looking for is already present. By making the
default value a by-name parameter, we do not have to evaluate it in the case
where it does not get used.
Using by-name parameters to "wrap" the evaluation of your method in some
setup/teardown code is another common pattern. The following measureTime
function defers evaluation of f
, allowing us to run
:
=>
Unit
System
before and after the argument is evaluated and thus
print out the time taken:.
currentTimeMillis(
)
@ def
measureTime(
f:
=>
Unit
)
=
{
val
start =
System.
currentTimeMillis(
)
f
val
end =
System.
currentTimeMillis(
)
println(
"Evaluation took "
+
(
end -
start)
+
" milliseconds"
)
}
@ measureTime(
new
Array[
String
]
(
10
*
1000
*
1000
)
.
hashCode(
)
)
Evaluation took 24
milliseconds
@ measureTime {
// methods taking a single arg can also be called with curly brackets
new
Array[
String
]
(
100
*
1000
*
1000
)
.
hashCode(
)
}
Evaluation took 287
milliseconds
</> 5.30.scala
There are many other use cases for such wrapping:
try
-catch
block so we can handle
exceptionsFuture
so the logic runs asynchronously on
another threadThese are all cases where using by-name parameter can help.
The last use case we will cover for by-name parameters is repeating evaluation
of the method argument. The following snippet defines a generic retry
method:
this method takes in an argument, evaluates it within a
-try
block, and
re-executes it on failure with a maximum number of attempts. We test this by
using it to wrap a call which may fail, and seeing the catch
retrying
messages get
printed to the console.
|
|
Above we define retry
as a generic function taking a type parameter
,
taking a by-name parameter that computes a value of type [
T]
T
, and returning a
T
once the code block is successful. We can then use retry
to wrap a code
block of any type, and it will retry that block and return the first T
it
successfully computes.
Making retry
take a by-name parameter is what allows it to repeat evaluation
of the requests
block where necessary. Other use cases for repetition
include running performance benchmarks or performing load tests. In general,
by-name parameters aren't something you use very often, but when necessary they
let you write code that manipulates the evaluation of a method argument in a
variety of useful ways: instrumenting it, retrying it, eliding it, etc..
get
We will learn more about the requests
library that we used in the above
snippet in Chapter 12: Working with HTTP APIs.
An implicit parameter is a parameter that is automatically filled in for you
when calling a function. For example, consider the following class Foo
and the
function bar
that takes an
parameter:implicit
foo:
Foo
@ class
Foo(
val
value:
Int
)
@ def
bar(
implicit
foo:
Foo)
=
foo.
value +
10
</> 5.33.scala
If you try to call bar
without an implicit Foo
in scope, you get a
compilation error. To call bar
, you need to define an implicit value of the
type Foo
, such that the call to bar
can automatically resolve it from the
enclosing scope:
|
|
Implicit parameters are similar to the default values we saw in Chapter 3: Basic Scala. Both of them allow you to pass in a value explicitly or fall back
to some default. The main difference is that while default values are "hard
coded" at the definition site of the method, implicit parameters take their
default value from whatever
is in scope at the call-site.implicit
We'll now look into a more concrete example where using implicit parameters can help keep your code clean and readable, before going into a more advanced use case of the feature for Typeclass Inference (5.5).
As an example, code using Future
needs an ExecutionContext
value in order to
work. As a result, we end up passing this ExecutionContext
everywhere, which
is tedious and verbose:
def
getEmployee(
ec:
ExecutionContext,
id:
Int
)
:
Future[
Employee]
=
.
.
.
def
getRole(
ec:
ExecutionContext,
employee:
Employee)
:
Future[
Role]
=
.
.
.
val
executionContext:
ExecutionContext =
.
.
.
val
bigEmployee:
Future[
EmployeeWithRole]
=
{
getEmployee(
executionContext,
100
)
.
flatMap(
executionContext,
e =>
getRole(
executionContext,
e)
.
map(
executionContext,
r =>
EmployeeWithRole(
e,
r)
)
)
}
</> 5.36.scala
getEmployee
and getRole
perform asynchronous actions, which we then map
and flatMap
to do further work. Exactly how the Future
s work is beyond the
scope of this section: for now, what is notable is how every operation needs to
be passed the executionContext
to do their work. We will will revisit these
APIs in Chapter 13: Fork-Join Parallelism with Futures.
Without implicit parameters, we have the following options:
Passing executionContext
explicitly is verbose and can make your code harder
to read: the logic we care about is drowned in a sea of boilerplate
executionContext
passing
Making executionContext
global would be concise, but would lose the
flexibility of passing different values in different parts of your program
Putting executionContext
into a thread-local variable would maintain
flexibility and conciseness, but it is error-prone and easy to forget to set
the thread-local before running code that needs it
All of these options have tradeoffs, forcing us to either sacrifice conciseness,
flexibility, or safety. Scala's implicit parameters provide a fourth option:
passing executionContext
implicitly, which gives us the conciseness,
flexibility, and safety that the above options are unable to give us.
To resolve these issues, we can make all these functions take the
executionContext
as an implicit parameter. This is already the case for
standard library operations like flatMap
and map
on Future
s, and we can
modify our getEmployee
and getRole
functions to follow suit. By defining
executionContext
as an
, it will automatically get picked up by all
the method calls below.implicit
def
getEmployee(
id:
Int
)
(
implicit
ec:
ExecutionContext)
:
Future[
Employee]
=
.
.
.
def
getRole(
employee:
Employee)
(
implicit
ec:
ExecutionContext)
:
Future[
Role]
=
.
.
.
implicit
val
executionContext:
ExecutionContext =
.
.
.
val
bigEmployee:
Future[
EmployeeWithRole]
=
{
getEmployee(
100
)
.
flatMap(
e =>
getRole(
e)
.
map(
r =>
EmployeeWithRole(
e,
r)
)
)
}
</> 5.37.scala
Using implicit parameters can help clean up code where we pass the same shared context or configuration object throughout your entire application:
By making the "uninteresting" parameter passing implicit, it can focus the reader's attention on the core logic of your application.
Since implicit parameters can be passed explicitly, they preserve the flexibility for the developer in case they want to manually specify or override the implicit parameter being passed.
The fact that missing implicits are a compile time error makes their usage much less error-prone than thread-locals. A missing implicit will be caught early on before code is compiled and deployed to production.
A second way that implicit parameters are useful is by using them to associate
values to types. This is often called a typeclass, the term originating from
the Haskell programming language, although it has nothing to do with types and
es in Scala. While typeclasses are a technique built on the same
class
language feature described earlier, they are an interesting and
important enough technique to deserve their own section in this chapter.implicit
Let us consider the task of parsing command-line arguments, given as
s,
into Scala values of various types: String
s, Int
s, Boolean
s, etc. This is
a common task that almost every program has to deal with, either directly or by
using a library.Double
A first sketch may be writing a generic method to parse the values. The signature might look something like this:
def
parseFromString[
T]
(
s:
String
)
:
T =
.
.
.
val
args =
Seq(
"123"
,
"true"
,
"7.5"
)
val
myInt =
parseFromString[
Int
]
(
args(
0
)
)
val
myBoolean =
parseFromString[
Boolean
]
(
args(
1
)
)
val
myDouble =
parseFromString[
Double
]
(
args(
2
)
)
</> 5.38.scala
On the surface this seems impossible to implement:
How does the parseCliArgument
know how to convert the given
into an
arbitrary String
T
?
How does it know what types T
a command-line argument can be parsed into,
and which it cannot? For example, we should not be able to parse a
java
from an input string..
net.
DatagramSocket
A second sketch at a solution may be to define separate parser objects, one for each type we need to be able to parse. For example:
trait
StrParser[
T]
{
def
parse(
s:
String
)
:
T }
object
ParseInt extends
StrParser[
Int
]
{
def
parse(
s:
String
)
=
s.
toInt }
object
ParseBoolean extends
StrParser[
Boolean
]
{
def
parse(
s:
String
)
=
s.
toBoolean }
object
ParseDouble extends
StrParser[
Double
]
{
def
parse(
s:
String
)
=
s.
toDouble }
</> 5.39.scala
We can then call these as follows:
val
args =
Seq(
"123"
,
"true"
,
"7.5"
)
val
myInt =
ParseInt.
parse(
args(
0
)
)
val
myBoolean =
ParseBoolean.
parse(
args(
1
)
)
val
myDouble =
ParseDouble.
parse(
args(
2
)
)
</> 5.40.scala
This works. However, it then leads to another problem: if we wanted to write a
method that didn't parse a
directly, but parsed a value from the
console, how would we do that? We have two options.String
The first option is writing a whole new set of
s
dedicated to parsing from the console:object
trait
ConsoleParser[
T]
{
def
parse(
)
:
T }
object
ConsoleParseInt extends
ConsoleParser[
Int
]
{
def
parse(
)
=
scala.
Console.
in.
readLine(
)
.
toInt
}
object
ConsoleParseBoolean extends
ConsoleParser[
Boolean
]
{
def
parse(
)
=
scala.
Console.
in.
readLine(
)
.
toBoolean
}
object
ConsoleParseDouble extends
ConsoleParser[
Double
]
{
def
parse(
)
=
scala.
Console.
in.
readLine(
)
.
toDouble
}
val
myInt =
ConsoleParseInt.
parse(
)
val
myBoolean =
ConsoleParseBoolean.
parse(
)
val
myDouble =
ConsoleParseDouble.
parse(
)
</> 5.41.scala
The second option is defining a helper method that receives a StrParser
as
an argument, which we would need to pass in to tell it how to parse the type [
T]
T
def
parseFromConsole[
T]
(
parser:
StrParser[
T]
)
=
parser.
parse(
scala.
Console.
in.
readLine(
)
)
val
myInt =
parseFromConsole[
Int
]
(
ParseInt)
val
myBoolean =
parseFromConsole[
Boolean
]
(
ParseBoolean)
val
myDouble =
parseFromConsole[
Double
]
(
ParseDouble)
</> 5.42.scala
Both of these solutions are clunky:
The first because we need to duplicate all the
/Int
/Boolean
/etc.
parsers. What if we need to parse input from the network? From files? We
would need to duplicate every parser for each case.Double
The second because we need to pass these ParseFoo
objects everywhere. Often
there is only a single StrParser
we can pass to
[
Int
]
parseFromConsole
. Why can't the compiler infer it for us?[
Int
]
The solution to the problems above is to make the instances of StrParser
:implicit
trait
StrParser[
T]
{
def
parse(
s:
String
)
:
T }
object
StrParser{
implicit
object
ParseInt extends
StrParser[
Int
]
{
def
parse(
s:
String
)
=
s.
toInt
}
implicit
object
ParseBoolean extends
StrParser[
Boolean
]
{
def
parse(
s:
String
)
=
s.
toBoolean
}
implicit
object
ParseDouble extends
StrParser[
Double
]
{
def
parse(
s:
String
)
=
s.
toDouble
}
}
</> 5.43.scala
We put the
, implicit
object
ParseIntParseBoolean
, etc. in an
with the same name as the object
StrParser
next to it. An trait
StrParser
with the same name as a object
that it is defined next to is called a
companion object. Companion objects are often used to group together
implicits, static methods, factory methods, and other functionality that is
related to a class
or trait
but does not belong to any specific instance.
Implicits in the companion object are also treated specially, and do not need to
be imported into scope in order to be used as an implicit parameter.class
Note that if you are entering this into the Ammonite Scala REPL, you need to
surround both declarations with an extra pair of curly brackets
so that
both the {
.
.
.
}
and trait
are defined in the same REPL command.object
Now, while we can still explicitly call ParseInt
to parse
literal strings as before, we can now write a generic function that
automatically uses the correct instance of .
parse(
args(
0
)
)
StrParser
depending on what type we
asked it to parse:
def
parseFromString[
T]
(
s:
String
)
(
implicit
parser:
StrParser[
T]
)
=
{
parser.
parse(
s)
}
val
args =
Seq(
"123"
,
"true"
,
"7.5"
)
val
myInt =
parseFromString[
Int
]
(
args(
0
)
)
val
myBoolean =
parseFromString[
Boolean
]
(
args(
1
)
)
val
myDouble =
parseFromString[
Double
]
(
args(
2
)
)
</> 5.44.scala
This looks similar to our initial sketch, except by taking an
parameter the function can now automatically infer the correct
(
implicit
parser:
StrParser[
T]
)
StrParser
for each type it is trying to parse.
Making our StrParser
s implicit means we can re-use them without duplicating
our parsers or passing them around manually. For example, we can write a
function that parses strings from the console:[
T]
def
parseFromConsole[
T]
(
implicit
parser:
StrParser[
T]
)
=
{
parser.
parse(
scala.
Console.
in.
readLine(
)
)
}
val
myInt =
parseFromConsole[
Int
]
</> 5.45.scala
The call to parseFromConsole
automatically infers the
[
Int
]
StrParser
implicit in the .
ParseIntStrParser
companion object, without
needing to duplicate it or tediously pass it around. That makes it very easy to
write code that works with a generic type T
as long as T
has a suitable
StrParser
.
This technique of taking an implicit parameter with a generic type is common enough that the Scala language provides dedicated syntax for it. The following method signature:
def
parseFromString[
T]
(
s:
String
)
(
implicit
parser:
StrParser[
T]
)
=
.
.
.
Can be written more concisely as:
def
parseFromString[
T:
StrParser]
(
s:
String
)
=
.
.
.
This syntax is referred to as a context bound, and it is semantically
equivalent to the
syntax above. When using the
context bound syntax, the implicit parameter isn't given a name, and so we
cannot call (
implicit
parser:
StrParser[
T]
)
parser
like we did earlier. Instead, we can resolve the
implicit values via the .
parseimplicitly
function, e.g.
implicitly
.[
StrParser[
T]
]
.
parse
As Typeclass Inference uses the the same
language feature we saw
earlier, mistakes such as attempting to call implicit
parseFromConsole
with an invalid
type produce a compile error:
@ val
myDatagramSocket =
parseFromConsole[
java.
net.
DatagramSocket]
cmd19.sc:1: could not find implicit value for parameter parser:
ammonite.$sess.cmd11.StrParser[java.net.DatagramSocket]
val myDatagramSocket = parseFromConsole[java.net.DatagramSocket]
^
Compilation Failed
</> 5.46.scala
Similarly, if you try to call a method taking an
from another method that does not have such an implicit
available, the compiler will also raise an error:(
implicit
parser:
StrParser[
T]
)
@ def
genericMethodWithoutImplicit[
T]
(
s:
String
)
=
parseFromString[
T]
(
s)
cmd2.sc:1: could not find implicit value for parameter parser:
ammonite.$sess.cmd0.StrParser[T]
def genericMethodWithoutImplicit[T](s: String) = parseFromString[T](s)
^
Compilation Failed
</> 5.47.scala
Most of the things we have done with Typeclass Inference could also be achieved
using runtime reflection. However, relying on runtime reflection is fragile, and
it is very easy for mistakes, bugs, or mis-configurations to make it to
production before failing catastrophically. In contrast, Scala's
feature lets you achieve the same outcome but in a safe fashion: mistakes are
caught early at compile-time, and you can fix them at your leisure rather than
under the pressure of a ongoing production outage.implicit
We have already seen how we can use the typeclass technique to automatically
pick which StrParser
to use based on the type we want to parse to. This can
also work for more complex types, where we tell the compiler we want a
Seq
, [
Int
]
, or even nested types like (
Int
,
Boolean
)
Seq
,
and the compiler will automatically assemble the logic necessary to parse the
type we want.[
(
Int
,
Boolean
)
]
For example, the following ParseSeq
function provides a StrParser
for any [
Seq[
T]
]
T
which itself has an implicit StrParser
in scope:[
T]
implicit
def
ParseSeq[
T]
(
implicit
p:
StrParser[
T]
)
=
new
StrParser[
Seq[
T]
]
{
def
parse(
s:
String
)
=
s.
split(
','
)
.
toSeq.
map(
p.
parse)
}
</> 5.48.scala
Note that unlike the
s we defined earlier which are singletons,
here we have an implicit
object
. Depending on the type implicit
def
T
, we would need a
different StrParser
, and thus need a different [
T]
StrParser
.
[
Seq[
T]
]
would thus return a different implicit
def
ParseSeqStrParser
each time it
is called with a different type T
.
From this one defintion, we can now parse Seq
s, [
Boolean
]
Seq
s, etc.[
Int
]
@ parseFromString[
Seq[
Boolean
]
]
(
"true,false,true"
)
res99:
Seq[
Boolean
]
=
ArraySeq(
true
,
false
,
true
)
@ parseFromString[
Seq[
Int
]
]
(
"1,2,3,4"
)
res100:
Seq[
Int
]
=
ArraySeq(
1
,
2
,
3
,
4
)
</> 5.49.scala
What we are effectively doing is teaching the compiler how to produce a
StrParser
for any type [
Seq[
T]
]
T
as long as it has an implicit
StrParser
available. Since we already have [
T]
StrParser
,
[
Int
]
StrParser
, and [
Boolean
]
StrParser
available, the [
Double
]
ParseSeq
method
gives StrParser
, [
Seq[
Int
]
]
StrParser
, and
[
Seq[
Boolean
]
]
StrParser
for free.[
Seq[
Double
]
]
The StrParser
we are instantiating has a parse method that receives a
parameter [
Seq[
T]
]
s
and returns a :
String
Seq
. We just needed to implement the
logic necessary to do that transformation, which we have done in the code
snippet above.[
T]
Similar to how we defined an
to parse implicit
def
Seq
s, we could do the
same to parse tuples. We do so below by assuming that tuples are represented by
[
T]
key
pairs in the input string:=
value
implicit
def
ParseTuple[
T,
V]
(
implicit
p1:
StrParser[
T]
,
p2:
StrParser[
V]
)
=
new
StrParser[
(
T,
V)
]
{
def
parse(
s:
String
)
=
{
val
Array(
left,
right)
=
s.
split(
'='
)
(
p1.
parse(
left)
,
p2.
parse(
right)
)
}
}
</> 5.50.scala
This definition produces a StrParser
, but only for a type [
(
T,
V)
]
T
and type
V
for which there are StrParser
s available. Now we can parse tuples, as
-separated pairs:=
@ parseFromString[
(
Int
,
Boolean
)
]
(
"123=true"
)
res102:
(
Int
,
Boolean
)
=
(
123
,
true
)
@ parseFromString[
(
Boolean
,
Double
)
]
(
"true=1.5"
)
res103:
(
Boolean
,
Double
)
=
(
true
,
1.5
)
</> 5.51.scala
The two definitions above,
and implicit
def
ParseSeq
, are enough to let us also parse sequences of tuples, or tuples of
sequences:implicit
def
ParseTuple
@ parseFromString[
Seq[
(
Int
,
Boolean
)
]
]
(
"1=true,2=false,3=true,4=false"
)
res104:
Seq[
(
Int
,
Boolean
)
]
=
ArraySeq(
(
1
,
true
)
,
(
2
,
false
)
,
(
3
,
true
)
,
(
4
,
false
)
)
@ parseFromString[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
(
"1,2,3,4,5=true,false,true"
)
res105:
(
Seq[
Int
]
,
Seq[
Boolean
]
)
=
(
ArraySeq(
1
,
2
,
3
,
4
,
5
)
,
ArraySeq(
true
,
false
,
true
)
)
</> 5.52.scala
Note that in this case we cannot handle nested Seq
s or nested tuples
due to how we're naively splitting the input string. A more structured parser
handles such cases without issues, allowing us to specify an arbitrarily complex
output type and automatically inferring the necessary parser. We will use a
serialization library that uses this technique in Chapter 8: JSON and Binary Data Serialization.[
Seq[
T]
]
Most statically typed programming languages can infer types to some degree: even if not every expression is annotated with an explicit type, the compiler can still figure out the types based on the program structure. Typeclass derivation is effectively the reverse: by providing an explicit type, the compiler can infer the program structure necessary to provide a value of the type we are looking for.
In the example above, we just need to define how to handle the basic types - how
to produce a StrParser
, [
Boolean
]
StrParser
, [
Int
]
StrParser
,
[
Seq[
T]
]
StrParser
- and the compiler is able to figure out how to produce a
[
(
T,
V)
]
StrParser
when we need it.[
Seq[
(
Int
,
Boolean
)
]
]
In this chapter, we have explored some of the more unique features of Scala. Case Classes or Pattern Matching you will use on a daily basis, while By-Name Parameters, Implicit Parameters, or Typeclass Inference are more advanced tools that you might only use when dictated by a framework or library. Nevertheless, these are the features that make the Scala language what it is, providing a way to tackle difficult problems more elegantly than most mainstream languages allow.
We have walked through the basic motivation and use cases for these features in this chapter. You will get familiar with more use cases as we see the features in action throughout the rest of this book.
This chapter will be the last in which we discuss the Scala programming language in isolation: subsequent chapters will introduce you to much more complex topics like working with your operating system, remote services, and third-party libraries. The Scala language fundamentals you have learned so far will serve you well as you broaden your horizons, from learning about the Scala language itself to using the Scala language to solve real-world problems.
Exercise: Define a function that uses pattern matching on the Expr
s we saw earlier to
perform simple algebraic simplifications:
|
|
|
|
|
|
|
|
Exercise: Modify the
function earlier that takes a by-name parameter and
make it perform an exponential backoff, sleeping between retries, with a
configurable initial def
retrydelay
in milliseconds:
retry(
max =
50
,
delay =
100
/*milliseconds*/
)
{
requests.
get(
s"$httpbin/status/200,400,500")
}
</> 5.53.scala
See example 5.8 - BackoffExercise: Modify the typeclass-based parseFromString
method we saw earlier to take a
JSON-like format, where lists are demarcated by square brackets with
comma-separated elements. This should allow it to parse and construct
arbitrarily deep nested data structures automatically via typeclass inference:
@ parseFromString[
Seq[
Boolean
]
]
(
"[true,false,true]"
)
// 1 layer of nesting
res1:
Seq[
Boolean
]
=
List(
true
,
false
,
true
)
@ parseFromString[
Seq[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
]
(
// 3 layers of nesting
"[[[1],[true]],[[2,3],[false,true]],[[4,5,6],[false,true,false]]]"
)
res2:
Seq[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
=
List(
(
List(
1
)
,
List(
true
)
)
,
(
List(
2
,
3
)
,
List(
false
,
true
)
)
,
(
List(
4
,
5
,
6
)
,
List(
false
,
true
,
false
)
)
)
@ parseFromString[
Seq[
(
Seq[
Int
]
,
Seq[
(
Boolean
,
Double
)
]
)
]
]
(
// 4 layers of nesting
"[[[1],[[true,0.5]]],[[2,3],[[false,1.5],[true,2.5]]]]"
)
res3:
Seq[
(
Seq[
Int
]
,
Seq[
(
Boolean
,
Double
)
]
)
]
=
List(
(
List(
1
)
,
List(
(
true
,
0.5
)
)
)
,
(
List(
2
,
3
)
,
List(
(
false
,
1.5
)
,
(
true
,
2.5
)
)
)
)
</> 5.54.scala
A production-ready version of this parseFromString
method exists in
upickle
, which we will see in Chapter 8: JSON and Binary Data Serialization..
default.
read
Exercise: How about using typeclasses to generate JSON, rather than parse it? Write a
writeToString
method that uses a StrWriter
typeclass to take nested values
parsed by parseFromString
, and serialize them to the same strings they were
parsed from.
@ writeToString[
Seq[
Boolean
]
]
(
Seq(
true
,
false
,
true
)
)
res1:
String
=
"[true,false,true]"
@ writeToString(
Seq(
true
,
false
,
true
)
)
// type can be inferred
res2:
String
=
"[true,false,true]"
@ writeToString[
Seq[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
]
(
Seq(
(
Seq(
1
)
,
Seq(
true
)
)
,
(
Seq(
2
,
3
)
,
Seq(
false
,
true
)
)
,
(
Seq(
4
,
5
,
6
)
,
Seq(
false
,
true
,
false
)
)
)
)
res3:
String
=
"[[[1],[true]],[[2,3],[false,true]],[[4,5,6],[false,true,false]]]"
@ writeToString(
Seq(
(
Seq(
1
)
,
Seq(
(
true
,
0.5
)
)
)
,
(
Seq(
2
,
3
)
,
Seq(
(
false
,
1.5
)
,
(
true
,
2.5
)
)
)
)
)
res4:
String
=
"[[[1],[[true,0.5]]],[[2,3],[[false,1.5],[true,2.5]]]]"
</> 5.55.scala
See example 5.10 - Serialize