This section introduces the various aspects of how to define and call methods in Scala 3.
Defining Methods
Scala methods have many features, including these:
- Type parameters
- Default parameter values
- Multiple parameter groups
- Context-provided parameters
- By-name parameters
- and more…
Some of these features are demonstrated in this section, but when you’re defining a “simple” method that doesn’t use those features, the syntax looks like this:
def methodName(param1: Type1, param2: Type2): ReturnType = {
// the method body
// goes here
}
def methodName(param1: Type1, param2: Type2): ReturnType =
// the method body
// goes here
end methodName // this is optional
In that syntax:
- The keyword
def
is used to define a method - The Scala standard is to name methods using the camel case convention
- Method parameters are always defined with their type
- Declaring the method return type is optional
- Methods can consist of many lines, or just one line
- Providing the
end methodName
portion after the method body is also optional, and is only recommended for long methods
Here are two examples of a one-line method named add
that takes two Int
input parameters.
The first version explicitly shows the method’s Int
return type, and the second does not:
def add(a: Int, b: Int): Int = a + b
def add(a: Int, b: Int) = a + b
It is recommended to annotate publicly visible methods with their return type. Declaring the return type can make it easier to understand it when you look at it months or years later, or when you look at another person’s code.
Calling methods
Invoking a method is straightforward:
val x = add(1, 2) // 3
The Scala collections classes have dozens of built-in methods. These examples show how to call them:
val x = List(1, 2, 3)
x.size // 3
x.contains(1) // true
x.map(_ * 10) // List(10, 20, 30)
Notice:
size
takes no arguments, and returns the number of elements in the list- The
contains
method takes one argument, the value to search for map
takes one argument, a function; in this case an anonymous function is passed into it
Multiline methods
When a method is longer than one line, start the method body on the second line, indented to the right:
def addThenDouble(a: Int, b: Int): Int = {
// imagine that this body requires multiple lines
val sum = a + b
sum * 2
}
def addThenDouble(a: Int, b: Int): Int =
// imagine that this body requires multiple lines
val sum = a + b
sum * 2
In that method:
sum
is an immutable local variable; it can’t be accessed outside of the method- The last line doubles the value of
sum
; this value is returned from the method
When you paste that code into the REPL, you’ll see that it works as desired:
scala> addThenDouble(1, 1)
res0: Int = 4
Notice that there’s no need for a return
statement at the end of the method.
Because almost everything in Scala is an expression—meaning that each line of code returns (or evaluates to) a value—there’s no need to use return
.
This becomes more clear when you condense that method and write it on one line:
def addThenDouble(a: Int, b: Int): Int = (a + b) * 2
The body of a method can use all the different features of the language:
if
/else
expressionsmatch
expressionswhile
loopsfor
loops andfor
expressions- Variable assignments
- Calls to other methods
- Definitions of other methods
As an example of a real-world multiline method, this getStackTraceAsString
method converts its Throwable
input parameter into a well-formatted String
:
def getStackTraceAsString(t: Throwable): String = {
val sw = new StringWriter()
t.printStackTrace(new PrintWriter(sw))
sw.toString
}
def getStackTraceAsString(t: Throwable): String =
val sw = StringWriter()
t.printStackTrace(PrintWriter(sw))
sw.toString
In that method:
- The first line assigns a new instance of
StringWriter
to the value bindersw
- The second line stores the stack trace content into the
StringWriter
- The third line yields the
String
representation of the stack trace
Default parameter values
Method parameters can have default values.
In this example, default values are given for both the timeout
and protocol
parameters:
def makeConnection(timeout: Int = 5_000, protocol: String = "http") = {
println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
// more code here ...
}
def makeConnection(timeout: Int = 5_000, protocol: String = "http") =
println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
// more code here ...
Because the parameters have default values, the method can be called in these ways:
makeConnection() // timeout = 5000, protocol = http
makeConnection(2_000) // timeout = 2000, protocol = http
makeConnection(3_000, "https") // timeout = 3000, protocol = https
Here are a few key points about those examples:
- In the first example no arguments are provided, so the method uses the default parameter values of
5_000
andhttp
- In the second example,
2_000
is supplied for thetimeout
value, so it’s used, along with the default value for theprotocol
- In the third example, values are provided for both parameters, so they’re both used
Notice that by using default parameter values, it appears to the consumer that they can use three different overridden methods.
Named parameters
If you prefer, you can also use the names of the method parameters when calling a method.
For instance, makeConnection
can also be called in these ways:
makeConnection(timeout=10_000)
makeConnection(protocol="https")
makeConnection(timeout=10_000, protocol="https")
makeConnection(protocol="https", timeout=10_000)
In some frameworks named parameters are heavily used. They’re also very useful when multiple method parameters have the same type:
engage(true, true, true, false)
Without help from an IDE that code can be hard to read, but this code is much more clear and obvious:
engage(
speedIsSet = true,
directionIsSet = true,
picardSaidMakeItSo = true,
turnedOffParkingBrake = false
)
A suggestion about methods that take no parameters
When a method takes no parameters, it’s said to have an arity level of arity-0. Similarly, when a method takes one parameter it’s an arity-1 method. When you create arity-0 methods:
- If the method performs side effects, such as calling
println
, declare the method with empty parentheses - If the method does not perform side effects—such as getting the size of a collection, which is similar to accessing a field on the collection—leave the parentheses off
For example, this method performs a side effect, so it’s declared with empty parentheses:
def speak() = println("hi")
Doing this requires callers of the method to use open parentheses when calling the method:
speak // error: "method speak must be called with () argument"
speak() // prints "hi"
While this is just a convention, following it dramatically improves code readability: It makes it easier to understand at a glance that an arity-0 method performs side effects.
Using if
as a method body
Because if
/else
expressions return a value, they can be used as the body of a method.
Here’s a method named isTruthy
that implements the Perl definitions of true
and false
:
def isTruthy(a: Any) = {
if (a == 0 || a == "" || a == false)
false
else
true
}
def isTruthy(a: Any) =
if a == 0 || a == "" || a == false then
false
else
true
These examples show how that method works:
isTruthy(0) // false
isTruthy("") // false
isTruthy("hi") // true
isTruthy(1.0) // true
Using match
as a method body
A match
expression can also be used as the entire method body, and often is.
Here’s another version of isTruthy
, written with a match
expression :
def isTruthy(a: Any) = a match {
case 0 | "" | false => false
case _ => true
}
def isTruthy(a: Matchable) = a match
case 0 | "" | false => false
case _ => true
This method works just like the previous method that used an
if
/else
expression. We useMatchable
instead ofAny
as the parameter’s type to accept any value that supports pattern matching.
For more details on the
Matchable
trait, see the Reference documentation.
Controlling visibility in classes
In classes, objects, traits, and enums, Scala methods are public by default, so the Dog
instance created here can access the speak
method:
class Dog {
def speak() = println("Woof")
}
val d = new Dog
d.speak() // prints "Woof"
class Dog:
def speak() = println("Woof")
val d = new Dog
d.speak() // prints "Woof"
Methods can also be marked as private
.
This makes them private to the current class, so they can’t be called nor overridden in subclasses:
class Animal {
private def breathe() = println("I’m breathing")
}
class Cat extends Animal {
// this method won’t compile
override def breathe() = println("Yo, I’m totally breathing")
}
class Animal:
private def breathe() = println("I’m breathing")
class Cat extends Animal:
// this method won’t compile
override def breathe() = println("Yo, I’m totally breathing")
If you want to make a method private to the current class and also allow subclasses to call it or override it, mark the method as protected
, as shown with the speak
method in this example:
class Animal {
private def breathe() = println("I’m breathing")
def walk() = {
breathe()
println("I’m walking")
}
protected def speak() = println("Hello?")
}
class Cat extends Animal {
override def speak() = println("Meow")
}
val cat = new Cat
cat.walk()
cat.speak()
cat.breathe() // won’t compile because it’s private
class Animal:
private def breathe() = println("I’m breathing")
def walk() =
breathe()
println("I’m walking")
protected def speak() = println("Hello?")
class Cat extends Animal:
override def speak() = println("Meow")
val cat = new Cat
cat.walk()
cat.speak()
cat.breathe() // won’t compile because it’s private
The protected
setting means:
- The method (or field) can be accessed by other instances of the same class
- It is not visible by other code in the current package
- It is available to subclasses
Objects can contain methods
Earlier you saw that traits and classes can have methods.
The Scala object
keyword is used to create a singleton class, and an object can also contain methods.
This is a nice way to group a set of “utility” methods.
For instance, this object contains a collection of methods that work on strings:
object StringUtils {
/**
* Returns a string that is the same as the input string, but
* truncated to the specified length.
*/
def truncate(s: String, length: Int): String = s.take(length)
/**
* Returns true if the string contains only letters and numbers.
*/
def lettersAndNumbersOnly_?(s: String): Boolean =
s.matches("[a-zA-Z0-9]+")
/**
* Returns true if the given string contains any whitespace
* at all. Assumes that `s` is not null.
*/
def containsWhitespace(s: String): Boolean =
s.matches(".*\\s.*")
}
object StringUtils:
/**
* Returns a string that is the same as the input string, but
* truncated to the specified length.
*/
def truncate(s: String, length: Int): String = s.take(length)
/**
* Returns true if the string contains only letters and numbers.
*/
def lettersAndNumbersOnly_?(s: String): Boolean =
s.matches("[a-zA-Z0-9]+")
/**
* Returns true if the given string contains any whitespace
* at all. Assumes that `s` is not null.
*/
def containsWhitespace(s: String): Boolean =
s.matches(".*\\s.*")
end StringUtils
Extension methods
There are many situations where you would like to add functionality to closed classes.
For example, imagine that you have a Circle
class, but you can’t change its source code.
It could be defined like this in a third-party library:
case class Circle(x: Double, y: Double, radius: Double)
When you want to add methods to this class, you can define them as extension methods, like this:
implicit class CircleOps(c: Circle) {
def circumference: Double = c.radius * math.Pi * 2
def diameter: Double = c.radius * 2
def area: Double = math.Pi * c.radius * c.radius
}
In Scala 2 use an implicit class
, find out more details here.
extension (c: Circle)
def circumference: Double = c.radius * math.Pi * 2
def diameter: Double = c.radius * 2
def area: Double = math.Pi * c.radius * c.radius
In Scala 3 use the new extension
construct. For more details see chapters in this book, or the Scala 3 reference.
Now when you have a Circle
instance named aCircle
, you can call those methods like this:
aCircle.circumference
aCircle.diameter
aCircle.area
Even more
There’s even more to know about methods, including how to:
- Call methods on superclasses
- Define and use by-name parameters
- Write a method that takes a function parameter
- Create inline methods
- Handle exceptions
- Use vararg input parameters
- Write methods that have multiple parameter groups (partially-applied functions)
- Create methods that have type parameters
See the other chapters in this book for more details on these features.