| 3.1 Values | 39 |
| 3.2 Loops, Conditionals, Comprehensions | 46 |
| 3.3 Methods and Functions | 49 |
| 3.4 Classes | 53 |
| 3.5 Traits | 55 |
| 3.6 Singleton Objects | 56 |
| 3.7 Optional Braces and Indentation | 58 |
for i <- Range.inclusive(1, 100) do
println(
if i % 3 == 0 && i % 5 == 0 then "FizzBuzz"
else if i % 3 == 0 then "Fizz"
else if i % 5 == 0 then "Buzz"
else i
)
3.1.scala
Snippet 3.1: the popular "FizzBuzz" programming challenge, implemented in Scala
This chapter is a quick tour of the Scala language. For now we will focus on the basics of Scala that are similar to what you might find in any mainstream programming language.
The goal of this chapter is to get you familiar enough that you can take the same sort of code you are used to writing in some other language and write it in Scala without difficulty. This chapter will not cover more Scala-specific programming styles or language features: those will be left for Chapter 5: Notable Scala Features.
For this chapter, we will write our code in the Scala REPL:
$ ./mill --repl
Welcome to Scala
Type in expressions for evaluation. Or try :help.
scala>
3.2.bashScala has the following sets of primitive types:
|
|
These types are identical to the primitive types in Java, and would be similar
to those in C#, C++, or any other statically typed programming language. Each
type supports the typical operations, e.g. booleans support boolean logic ||
&&, numbers support arithmetic + - * / and bitwise operations | &,
and so on. All values support == to check for equality and != to check for
inequality.
Numbers default to 32-bit Ints. Precedence for arithmetic operations follows
other programming languages: * and / have higher precedence than + or -,
and parentheses can be used for grouping.
| |
Ints are signed and wrap-around on overflow, while 64-bit Longs suffixed
with L have a bigger range and do not overflow as easily:
| |
Apart from the basic operators, there are a lot of useful methods on
java.lang.Integer and java.lang.Long:
> java.lang.Integer.<tab>
BYTES decode numberOfTrailingZeros
signum MAX_VALUE divideUnsigned
getInteger parseUnsignedInt toBinaryString
...
> java.lang.Integer.toBinaryString(123)
res6: String = "1111011"
> java.lang.Integer.numberOfTrailingZeros(24)
res7: Int = 3
3.7.scala
64-bit Doubles are specified using the 1.0 syntax, and have a similar set of
arithmetic operations. You can also use the 1.0F syntax to ask for 32-bit
Floats:
| |
32-bit Floats take up half as much memory as 64-bit Doubles, but are more
prone to rounding errors during arithmetic operations. java.lang.Float and
java.lang.Double have a similar set of useful operations you can perform on
Floats and Doubles.
Strings in Scala are arrays of 16-bit Chars:
> "hello world"
res10: String = "hello world"
3.10.scala
Strings can be sliced with .substring, constructed via
concatenation using +, or via string interpolation by prefixing the literal
with s"..." and interpolating the values with $ or ${...}:
| |
You can name local values with the val keyword:
> val x = 1
> x + 2
res18: Int = 3
3.13.scala
Note that vals are immutable: you cannot re-assign the val x to a different
value after the fact. If you want a local variable that can be re-assigned, you
must use the var keyword.
| |
In general, you should try to use val where possible: most named values
in your program likely do not need to be re-assigned, and using val
helps prevent mistakes where you re-assign something accidentally. Use var
only if you are sure you will need to re-assign something later.
Both vals and vars can be annotated with explicit types. These can serve as
documentation for people reading your code, as well as a way to catch errors if
you accidentally assign the wrong type of value to a variable
| |
Tuples are fixed-length collections of values, which may be of different types:
| |
Above, we are storing a tuple into the local value t using the (a, b, c)
syntax, and then accessing the fields by their index to extract the values out of it,
using the syntax t(0), t(1) and t(2).
The fields in a tuple are immutable.
The type of the local value t can be annotated as a tuple type:
> val t: (Int, Boolean, String) = (1, true, "hello")
You can also use the val (a, b, c) = t syntax to extract all the values at
once, and assign them to meaningful names:
| |
Tuples can be any size, up to the maximum 32-bit signed integer:
> val t = (1, true, "hello", 'c', 0.2, 0.5f)
t: (Int, Boolean, String, Char, Double, Float) = (1, true, "hello", 'c', 0.2, 0.5F)
3.22.scala
Most tuples should be relatively small. Large tuples can easily get confusing:
while working with t(0) t(1) and t(2) is probably fine, when you end up
working with t(10) t(12) it becomes easy to mix up the different fields. If
you find yourself working with large tuples, consider defining a
Class (3.4) or Case Class that we will see in Chapter 5: Notable Scala Features.
Arrays are instantiated using the Array[T](a, b, c) syntax, and entries within
each array are retrieved using a(n):
> val a = Array[Int](1, 2, 3, 4)
> a(0) // first entry, array indices start from 0
res36: Int = 1
> a(3) // last entry
res37: Int = 4
> val a2 = Array[String]("one", "two", "three", "four")
> a2(1) // second entry
res39: String = "two"
3.23.scala
The type parameter inside the square brackets [Int] or [String] determines
the type of the array, while the parameters inside the parenthesis (1, 2, 3, 4) determine its initial contents. Note that looking up an Array by index is
done via parentheses a(3) rather than square brackets a[3] as is common in
many other programming languages.
You can omit the explicit type parameter and let the compiler infer the Array's
type, or create an empty array of a specified type using new Array[T](length),
and assign values to each index later:
| |
For Arrays created using new Array, all entries start off with the value 0
for numeric arrays, false for Boolean arrays, and null for Strings and
other types. Arrays are mutable but fixed-length: you can change the value of
each entry but cannot change the number of entries by adding or removing values.
We will see how to create variable-length collections later in Chapter 4: Scala Collections.
Multi-dimensional arrays are typically modeled as arrays-of-arrays:
> val multi = Array(Array(1, 2), Array(3, 4))
multi: Array[Array[Int]] = Array(Array(1, 2), Array(3, 4))
> multi(0)(0)
res47: Int = 1
> multi(0)(1)
res48: Int = 2
> multi(1)(0)
res49: Int = 3
> multi(1)(1)
res50: Int = 4
3.26.scala
Multi-dimensional arrays can be useful to represent grids, matrices, and similar values.
Arrays and other collections support a lot of methods that are useful for working with them: transforming, filtering, aggregating, etc. A few of these are shown below:
> val arr = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
> arr.filter(_ % 2 == 0)
res1: Array[Int] = Array(2, 4, 6, 8, 10)
> arr.filter(_ % 2 == 0).map(_ * 10)
res2: Array[Int] = Array(20, 40, 60, 80, 100)
> arr.filter(_ % 2 == 0).map(_ * 10).mkString(" ")
res3: String = "20 40 60 80 100"
> arr.filter(_ % 2 == 0).map(_ * 10).sum
res4: Int = 300
> arr.filter(_ % 2 == 0).map(_ * 10).reduce(_ * _)
res5: Int = 384000000
3.27.scala
In general, when working with arrays and collections, it is preferable to use these collection operations rather than constructing them from scratch via loops and assignment. We discuss these in more detail in Chapter 4: Scala Collections.
Scala's Option[T] type allows you to represent a value that may or may not
exist. An Option[T] can either be Some(v: T) indicating that a value is
present, or None indicating that it is absent:
> def hello(title: String, firstName: String, lastNameOpt: Option[String]) =
lastNameOpt match
case Some(lastName) => println(s"Hello $title. $lastName")
case None => println(s"Hello $firstName")
> hello("Mr", "Haoyi", None)
Hello Haoyi
> hello("Mr", "Haoyi", Some("Li"))
Hello Mr. Li
3.28.scala
The above example shows you how to construct Options using Some and None,
as well as matching on them in the same way. Many APIs in Scala rely on
Options rather than nulls for values that may or may not exist. In general,
Options force you to handle both cases of present/absent, whereas when using
nulls it is easy to forget whether or not a value is null-able, resulting in
confusing NullPointerExceptions at runtime. We will go deeper into pattern
matching in Chapter 5: Notable Scala Features.
Options contain some helper methods that make it easy to work with the
optional value, such as getOrElse, which substitutes an alternate value if the
Option is None:
| |
Options are very similar to a collection whose size is 0 or 1. You can
loop over them like normal collections, or transform them with standard
collection operations like .map. We will learn more about collection
operations in Chapter 4: Scala Collections.
| |
For-loops in Scala are similar to "foreach" loops in other languages: they
directly loop over the elements in a collection, without needing to explicitly
maintain and increment an index. If you want to loop over a range of indices,
you can loop over a Range such as Range(0, 4):
| |
The default Range is left-inclusive right-exclusive, and so does not loop over the value
4 in the example above. If you want to loop over a
right-inclusive range, you can use Range.inclusive:
> for i <- Range.inclusive(0, 4) do println(i) // Range.inclusive(0, 4) includes 4
...
4
3.35.scala
You can loop over nested Arrays or nested Ranges by placing multiple <-s in the header of the
loop, or add if guards to run the loop on only a subset of the elements:
> val multi = Array(Array(1, 2, 3), Array(4, 5, 6))
| |
if-else conditionals are similar to those in any other programming language.
One thing to note is that in Scala if-else can also be used as an
expression, similar to the a ? b : c ternary expressions in other languages.
Scala does not have a separate ternary expression syntax, and so the if-else
can be directly used as the right-hand-side of the total += below.
| |
Now that we know the basics of Scala syntax, let's consider the common "Fizzbuzz" programming challenge:
Write a short program that prints each number from 1 to 100 on a new line.
For each multiple of 3, print "Fizz", for each multiple of 5, print "Buzz".
For numbers which are multiples of both 3 and 5, print "FizzBuzz" instead of the number.
We can accomplish this as follows:
> for i <- Range.inclusive(1, 100) do
if i % 3 == 0 && i % 5 == 0 then println("FizzBuzz")
else if i % 3 == 0 then println("Fizz")
else if i % 5 == 0 then println("Buzz")
else println(i)
3.40.scala
Since if-else is an expression, we can also write it inside the println,
or assign it to a local val within the loop which you then pass to println:
| |
Apart from using for to define loops that perform some action, you can also
use for together with yield to transform a collection into a new collection:
> val a = Array(1, 2, 3, 4)
> val a3 = for i <- a yield "hello " + i
a3: Array[String] = Array("hello 1", "hello 2", "hello 3", "hello 4")
3.43.scala
Similar to loops, you can filter which items end up in the final collection
using an if guard:
> val a4 = for i <- a if i % 2 == 0 yield "hello " + i
a4: Array[String] = Array("hello 2", "hello 4")
3.44.scala
Like loops, comprehensions can also take multiple input arrays, a and b below,
and if guards to select the entries you want to collect into a final flat array.
You can spread out the nested loops over multiple lines for easier reading.
Note that the order of <-s within the nested comprehension matters, just like how
the order of nested loops affects the order in which the loop actions will take
place:
| |
| |
We can use comprehensions to write a version of FizzBuzz that doesn't print its
results immediately to the console, but returns them as a Seq (short for
"sequence"):
| |
You can define methods using the def keyword:
| |
Passing in the wrong type of argument, or missing required arguments, is a compiler error:
| |
However, if the argument has a default value, then passing it is optional.
| |
Apart from performing actions like printing, methods can also return values. The
last expression within an indented block is treated as the return value of a Scala method,
and the return keyword is optional and typically not used:
| |
You can call the method and print out or perform other computation on the returned value:
| |
You can define function values using the => syntax. Functions values are
similar to methods, in that you call them with arguments and they can perform
some action or return some value. Unlike methods, functions themselves are
values: you can pass them around, store them in variables, and call them later.
| |
Note that unlike methods, function values cannot have optional arguments (i.e.
with default values) and cannot take type parameters via the [T] syntax. When
a method is converted into a function value, any optional arguments must be
explicitly included, and type parameters fixed to concrete types. Function
values are also anonymous, which makes stack traces involving them less
convenient to read than those using methods.
In general, you should prefer using methods unless you really need the flexibility to pass as parameters or store them in variables. But if you need that flexibility, function values are a great tool to have.
One common use case of function values is to pass them into methods that take
function parameters. Such methods are often called "higher order methods".
Below, we have a class Box with a method printMsg that prints its contents
(an Int), and a separate method update that takes a function of type Int => Int that can be used to update x. You can then pass a function literal into
update in order to change the value of x:
| |
Simple functions literals like i => i + 5 can also be written via the
shorthand _ + 5, with the underscore _ standing in for the function
parameter.
> b.update(_ + 5)
> b.printMsg("Hello")
Hello11
3.65.scala
This placeholder syntax for function literals also works for multi-argument
functions, e.g. (x, y) => x + y can be written as _ + _.
Any method that takes a function as an argument can also be given a method
reference, as long as the method's signature matches that of the function type,
here Int => Int:
> def increment(i: Int) = i + 1
> val b = Box(123)
> b.update(increment) // Providing a method reference
> b.update(x => increment(x)) // Explicitly writing out the function literal
> b.update: x => // can pass a lambda without parens or braces
increment(x)
> b.update(increment(_)) // You can also use the `_` placeholder syntax
> b.printMsg("result: ")
result: 127
3.66.scalaMethods can be defined to take multiple parameter lists. This is useful for
writing higher-order methods that can be used like control structures, such as
the loop method below:
> def loop(start: Int, end: Int)(callback: Int => Unit) =
for i <- Range(start, end) do callback(i)
> loop(start = 5, end = 8): i =>
println(s"i has value ${i}")
i has value 5
i has value 6
i has value 7
3.67.scala
The ability to pass function literals to methods is used to great effect in the standard library, to concisely perform transformations on collections. We will see more of that in Chapter 4: Scala Collections.
Methods can be defined to be generic meaning that they can work on multiple types.
These are have type parameters defined with [...] syntax before the value parameters
defined with (...) syntax. For example, below is a method that returns the first and
last elements of an array:
> def firstAndLastElements[T](arr: Array[T]): (T, T) = (arr(0), arr(arr.length - 1))
> firstAndLastElements(Array(1, 2, 3, 4, 5))
res1: (Int, Int) = (1, 5)
> firstAndLastElements(Array("i", "am", "cow"))
res2: (String, String) = ("i", "cow")
3.68.scala
Above we can see the method takes an Array[T] and returns a tuple (T, T) containing
two Ts, but the method doesn't actually care what T is. Thus when passed an Array[Int]
it returns a tuple of (Int, Int), and when passed an Array[String] it returns a tuple
of (String, String).
The [T] parameter of firstAndLastElements can often be inferred, and above we do not need
to provide it. But you can also specify the type parameter
explicitly when calling the method:
> firstAndLastElements[Int](Array(1, 2, 3, 4, 5))
res3: (Int, Int) = (1, 5)
> firstAndLastElements[String](Array("i", "am", "cow"))
res4: (String, String) = ("i", "cow")
3.69.scala
This can be useful in more non-trivial code where it's not obvious what type is being inferred, or if the type being inferred is not what you expect.
You can define classes using the class keyword, and instantiate them using
new. By default, all arguments passed into the class constructor are available
in all of the class' methods: the (x: Int) above defines both the private
fields as well as the class' constructor. x is thus accessible in the
printMsg function, but cannot be accessed outside the class:
| |
You can also omit the new keyword instantiate classes via just Foo(1):
> val f = Foo(1)
To make x publicly accessible you can make it a val, and to make it mutable
you can make it a var:
| |
| |
You can also use vals or vars in the body of a class to store data. These
get computed once when the class is instantiated:
| |
Scala classes support most standard object-oriented programming features
> class FooPrintsTwice(x: Int) extends Foo(x * 2):
override def printMsg(msg: String) =
super.printMsg(msg)
super.printMsg(msg * 2)
> new FooPrintsTwice(123).printMsg("hello")
hello246
hello246hello246
3.78.scala
Note in this example that when we call extends, we also get a chance to forward constructor
parameters to the parent constructor, modifying them along the way if necessary. When we
override def printMsg we also get a chance to call super.printMsg however we like (in
this case, twice) and forward whatever arguments we like (in this case, repeat the parameter
forwarded to the second super call).
Methods marked as final cannot be overridden:
> class FinalFoo(x: Int):
final def printMsg(msg: String) = println(msg + x)
> class FinalFooOverride(x: Int) extends FinalFoo(x):
override def printMsg(msg: String) = println(msg + x * 2)
-- [E164] Declaration Error: ---------------------------------------------------
|error overriding method printMsg in class FinalFoo of type (msg: String): Unit;
| method printMsg of type (msg: String): Unit cannot override final member method
3.79.scalaClasses marked as abstract can have abstract methods, which are method defs that do not
have an implementation. These must be implemented by any non-abstract sub-classes.
> abstract class AbstractFoo(x: Int):
def printMsg(msg: String): Unit
> class ConcreteFoo1(x: Int) extends AbstractFoo(x) // Missing abstract method
-- Error: ----------------------------------------------------------------------
|class ConcreteFoo1 needs to be abstract, since def printMsg(msg: String): Unit
|in class AbstractFoo is not defined
> class ConcreteFoo1(x: Int) extends AbstractFoo(x): // OK
def printMsg(msg: String) = println(msg + x)
3.80.scalatraits are similar to interfaces in traditional object-oriented languages: a
set of methods that multiple classes can inherit. Instances of these classes can
then be used interchangeably. Unlike inheriting from a class, inheriting from multiple
traits is allowed, and trait methods are allowed to be abstract by default:
> trait Point:
def magnitude: Double
> trait Jsonable:
def toJson: String
> class Point2D(x: Double, y: Double) extends Point, Jsonable:
def magnitude = math.sqrt(x * x + y * y)
def toJson = s"[$x, $y]"
> class Point3D(x: Double, y: Double, z: Double) extends Point, Jsonable:
def magnitude = math.sqrt(x * x + y * y + z * z)
def toJson = s"[$x, $y, $z]"
> val points = Array(Point2D(1, 2), Point3D(4, 5, 6))
> for p <- points do println(p.magnitude)
2.23606797749979
8.774964387392123
> for p <- points do println(p.toJson)
[1, 2]
[4, 5, 6]
3.81.scala
Above, we have defined a Point and Jsonable traits with methods def magnitude: Double and def toJson: String respectively. The subclasses Point2D and Point3D
both have different sets of parameters, but they both implement def magnitude and def toJson.
Thus we can put both Point2Ds and Point3Ds into our points array and treat them all
uniformly as objects with a def magnitude and def toJson methods, regardless of what their
actual class is.
In addition to abstract defs that purely define an interface, Scala traits can also contain
implementations to let you re-use common logic between the different sub-classes. The
Point2D/Point3D example we saw earlier can be re-organized as follows, with the
def magnitude and def toJson logic centralized within trait Point and trait Jsonable:
trait Point:
def coordinates: Seq[Double]
def magnitude: Double = math.sqrt(coordinates.map(v => v * v).sum)
trait Jsonable:
def coordinates: Seq[Double]
def toJson: String = "[" + coordinates.mkString(", ") + "]"
class Point2D(x: Double, y: Double) extends Point, Jsonable:
def coordinates = Seq(x, y)
class Point3D(x: Double, y: Double, z: Double) extends Point, Jsonable:
def coordinates = Seq(x, y, z)
3.82.scala
In this way the sub-classes Point2D and Point3D only need to implement a single
def coordinates: Seq[Double] method, and they automatically inherit the implementations
of magnitude and toJson from the traits that they inherit. This can reduce boilerplate
and ensure the different sub-classes have a consistent implementation of the logic within
each trait.
Like classes, traits support standard object-oriented programming features like override
and super:
> class PrettyPoint3D(x: Double, y: Double, z: Double) extends Point, Jsonable:
def coordinates = Seq(x, y, z)
override def toJson = "***" + super.toJson + "***"
> PrettyPoint3D(1, 2, 3).toJson
res0: String = "***[1.0, 2.0, 3.0]***"
3.83.scalaSingleton objects in Scala can be defined with the object keyword. These have a variety
of use cases that we will discuss below:
Singleton objects are useful to put defs and vars and other definitions into namespaces.
This is similar to static methods in languages like Java or C#, or packages in languages like
Python. When a complicated part of your program starts getting messy with many variables and
methods, grouping related definitions into named objects can help keep things neat:
| |
Unlike static methods in other languages, singleton objects can inherit from classes
and traits:
| |
This has two uses:
If you have methods in a class or trait that you want your singleton object to also have,
you can just extends it in your singleton object and inherit those definitions
If you have a class or trait that has some special values, e.g. a "default" value
or a "null" value, making it a singleton object is a convenient way to do so. For example:
object EmptyInputStream extends java.io.InputStream:
// return -1 to immediately signal end of stream without returning any data
def read(): Int = -1
3.88.scalaAn object with the same name as a class that it is defined next to is called a
companion object. Companion objects are similar to static methods in other languages,
and are often used to group together methods, variables factory methods, and other
functionality that is related to a trait or class but does not belong to any
specific instance.
class Foo():
// instance methods and variables specific to each instance of Foo
object Foo:
// methods and variables related to Foo but not any specific instance
3.89.scala
For example, the List class has an object List singleton object that contains things like:
List.apply, Array.fillList.fromList.emptyArray.newBuilder, Array.toFactoryprivate methods and fields used in the internal implementation of List that are shared
and not specific to any particular instanceobject List inherits much of this functionality from a super-class shared with the other
Scala collection types, which also allows it to be used as a value in methods that expect
that super-class such as Array.to:
> Array(1, 2, 3).to(List)
res0: List[Int] = List(1, 2, 3)
3.90.scala
We will explore more of the operations available on Array and List, and how to use them,
later in Chapter 4: Scala Collections.
Scala can use braces to delimit blocks, but they are optional, and in their absence the language uses indentation to determine where blocks of code start and end. Semicolons are similarly optional and generally not used. For example, the two snippets below show the same Scala code with the braces present (left) and without braces instead using indentation to delimit blocks of code (right):
| |
In general, relying on indentation is preferred. Braces remain available, but are typically only used in the following scenarios:
Backwards compatibility with older versions of Scala (2.x) that do not support indentation syntax, e.g. if you want to write a library that runs on old versions
Squeeze things onto a single line. This isn't something you do often, but on some occasions it can be useful:
myBox.update { previous => println(s"Incrementing $previous!"); previous + 1 }
In this chapter, we have gone through a lightning tour of the core Scala language. While the exact syntax may be new to you, the concepts should be mostly familiar: primitives, arrays, loops, conditionals, methods, and classes are part of almost every programming language.
Next we will look at the core of the Scala standard library: the Scala Collections.
Exercise: Write a recursive method printMsgs that can receive an array of Msg
class instances, each with an optional parent ID, and use it to print it in a
threaded fashion. That means that child messages are printed out indented
underneath their parents, and the nesting can be arbitrarily deep.
class Msg(val id: Int, val parent: Option[Int], val txt: String)
def printMsgs(messages: Array[Msg]): Unit = ...
3.93.scala
|
|
Exercise: Define a pair of methods withFileWriter and withFileReader that can be
called as shown below. Each method should take the name of a file, and a
function value that is called with a java.io.BufferedReader or
java.io.BufferedWriter that it can use to read or write data. Opening and
closing of the reader/writer should be automatic, such that a caller cannot
forget to close the file. This is similar to Python "context managers" or Java
"try-with-resource" syntax.
TestContextManagers.scala3.96.scala//| moduleDeps: [ContextManagers.scala]defmain()=withFileWriter("File.txt"):writer=>writer.write("Hello\n")writer.write("World!")valresult=withFileReader("File.txt"):reader=>reader.readLine()+"\n"+reader.readLine()assert(result=="Hello\nWorld!")
For now you can use the Java standard library APIs
java.nio.file.Files.newBufferedWriter and newBufferedReader for working
with file readers and writers. We will get more familiar with working with
files and the filesystem in Chapter 7: Files and Subprocesses.
Exercise: Define a def flexibleFizzBuzz method that takes a String => Unit callback
function as its argument, and allows the caller to decide what they want to do
with the output. The caller can choose to ignore the output, println the
output directly, or store the output in a previously-allocated array they
already have handy.
| |