This section introduces several syntactic sugars supported by the language.
To splice values into constant String
at runtime, you can
use string interpolation:
def greet(name: String): String =
s"Hello, $name!"
greet("Scala") shouldBe "Hello, Scala!"
greet("Functional Programming") shouldBe res0
After having prefixed the string literal with s
you can introduce
dynamic values in it with $
.
If you want to splice a complex expression (more than just an identifier), surround it with braces:
def greet(name: String): String =
s"Hello, ${name.toUpperCase}!"
greet("Scala") shouldBe res0
We saw earlier that case classes are useful to aggregate information. However, sometimes you want to aggregate information without having to define a complete case class for it. In such a case you can use tuples:
def pair(i: Int, s: String): (Int, String) = (i, s)
pair(42, "foo") shouldBe ((42, "foo"))
pair(0, "bar") shouldBe res0
In the above example, the type (Int, String)
represents a pair whose
first element is an Int
and whose second element is a String
.
Similarly, the value (i, s)
is a pair whose first element is i
and
whose second element is s
.
More generally, a type (T1, …, Tn)
is a tuple type of n elements
whose ith element has type Ti
.
And a value (t1, … tn)
is a tuple value of n elements.
You can retrieve the elements of a tuple by using a tuple pattern:
val is: (Int, String) = (42, "foo")
is match {
case (i, s) =>
i shouldBe 42
s shouldBe res0
}
Or, simply:
val is: (Int, String) = (42, "foo")
val (i, s) = is
i shouldBe 42
s shouldBe res0
Alternatively, you can retrieve the 1st element with the _1
member,
the 2nd element with the _2
member, etc:
val is: (Int, String) = (42, "foo")
is._1 shouldBe 42
is._2 shouldBe res0
We have seen that Scala's numeric types and the Boolean
type can be implemented like normal classes.
But what about functions?
In fact function values are treated as objects in Scala.
The function type A => B
is just an abbreviation for the class
scala.Function1[A, B]
, which is defined as follows.
package scala
trait Function1[A, B] {
def apply(x: A): B
}
So functions are objects with apply
methods.
There are also traits Function2
, Function3
, ... for functions which take more parameters (currently up to 22).
An anonymous function such as
(x: Int) => x * x
is expanded to:
{
class AnonFun extends Function1[Int, Int] {
def apply(x: Int) = x * x
}
new AnonFun
}
or, shorter, using anonymous class syntax:
new Function1[Int, Int] {
def apply(x: Int) = x * x
}
A function call, such as f(a, b)
, where f
is a value of some class
type, is expanded to:
f.apply(a, b)
So the OO-translation of:
val f = (x: Int) => x * x
f(7)
would be:
val f = new Function1[Int, Int] {
def apply(x: Int) = x * x
}
f.apply(7)
Note that a method such as
def f(x: Int): Boolean = …
is not itself a function value.
But if f
is used in a place where a Function type is expected, it is
converted automatically to the function value
(x: Int) => f(x)
for
expressions You probably noticed that several data types of the standard library
have methods named map
, flatMap
and filter
.
These methods are so common in practice that Scala supports a dedicated syntax: for expressions.
map
Thus, instead of writing the following:
xs.map(x => x + 1)
You can write:
for (x <- xs) yield x + 1
You can read it as “for every value, that I name ‘x’, in ‘xs’, return ‘x + 1’”.
filter
Also, instead of writing the following:
xs.filter(x => x % 2 == 0)
You can write:
for (x <- xs if x % 2 == 0) yield x
The benefit of this syntax becomes more apparent when it is combined with the previous one:
for (x <- xs if x % 2 == 0) yield x + 1
// Equivalent to the following:
xs.filter(x => x % 2 == 0).map(x => x + 1)
flatMap
Finally, instead of writing the following:
xs.flatMap(x => ys.map(y => (x, y)))
You can write:
for (x <- xs; y <- ys) yield (x, y)
You can read it as “for every value ‘x’ in ‘xs’, and then for every value ‘y’ in ‘ys’, return ‘(x, y)’”.
Here is an example that puts everything together:
for {
x <- xs if x % 2 == 0
y <- ys
} yield (x, y)
The equivalent de-sugared code is the following:
xs.filter { x =>
x % 2 == 0
}.flatMap { x =>
ys.map { y =>
(x, y)
}
}
It can sometimes be difficult to figure out what is the meaning of each parameter passed to a function. Consider for instance the following expression:
Range(1, 10, 2)
What does it mean? We can improve the readability by using named parameters.
Based on the fact that the Range
constructor is defined as follows:
case class Range(start: Int, end: Int, step: Int)
We can rewrite our expression as follows:
Range(start = 1, end = 10, step = 2)
It is now clearer that this expression defines a range of numbers from 1 to 10 by increments of 2.
Methods’ parameters can have default values. Let’s refine the Range
constructor:
case class Range(start: Int, end: Int, step: Int = 1)
Here, we say that the step
parameter has a default value, 1
.
Then, at use site we can omit to supply this parameter and the compiler will supply it for us, by using its default value:
case class Range(start: Int, end: Int, step: Int = 1)
val xs = Range(start = 1, end = 10)
xs.step shouldBe res0
You can define a function that can receive an arbitrary number of parameters (of the same type) as follows:
def average(x: Int, xs: Int*): Double =
(x :: xs.toList).sum.toDouble / (xs.size + 1)
average(1) shouldBe 1.0
average(1, 2) shouldBe 1.5
average(1, 2, 3) shouldBe res0
The average
function takes at least one Int
parameter and then
an arbitrary number of other values and computes their average.
By forcing users to supply at least one parameter, we make it impossible
for them to compute the average of an empty list of numbers.
Sometimes you want to supply each element of a list as many parameters.
You can do that by adding a : _*
type ascription to your list:
val xs: List[Int] = …
average(1, xs: _*)
In the same way as you can give meaningful names to expressions, you can give meaningful names to type expressions:
type Result = Either[String, (Int, Int)]
def divide(dividend: Int, divisor: Int): Result =
if (divisor == 0) Left("Division by zero")
else Right((dividend / divisor, dividend % divisor))
divide(6, 4) shouldBe Right((1, 2))
divide(2, 0) shouldBe Left("Division by zero")
divide(8, 4) shouldBe res0