title | type | description | num | previous-page | next-page |
---|---|---|---|---|---|
Method Features |
section |
This section introduces Scala 3 methods, including main methods, extension methods, and more. |
24 |
methods-intro |
methods-main-methods |
This section introduces the various aspects of how to define and call methods in Scala 3.
Scala methods have many features, including these:
- Generic methods with type parameters
- Default parameter values
- Multiple parameter groups
- Context-provided parameters
- By-name parameters
- ...
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
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.
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
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
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 = 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
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 ...
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.
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
)
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---about such methods they say "pure methods" or "pure code", contrary to "dirty" or "impure"---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"
The main intention behind this convention is to make transition field-to-method and visa-versa easy for developers and transparent to method consumers. For example, given the following code, no one can tell if speak
is a field or method:
val alleyCat = Cat("Oliver")
println(alleyCat.speak)
In FP terms "call of pure function" and "result of pure function" (with given arguments) are totally the same thing. While code does not change any existing state we can substitute field instead of method, and method instead of the field. The result must be the same.
However, in the case of impure code consumers must be warned and alarmed at least, because impure code tends to make dangerous things, like throwing IOException
. That is why there is a difference between an absent list and an empty argument list.
{% comment %} Some of that wording comes from this page: https://docs.scala-lang.org/style/method-invocation.html {% endcomment %}
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 then
false
else
true
These examples show how that method works:
isTruthy(0) // false
isTruthy("") // false
isTruthy("hi") // true
isTruthy(1.0) // true
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: Matchable) = a match
case 0 | "" | false => false
case _ => true
This method works just like the previous method that used an if
/else
expression. We use Matchable
instead of Any
as the parameter's type to accept any value that supports pattern matching.
For more details on the Matchable
trait, see the [Reference documentation][reference_matchable].
In classes, objects, traits, and enums, Scala methods are public by default, so world can access the speak
method of Dog
instance created here:
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("Meow. I’m breathing too.")
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("Hi people")
class Cat extends Animal:
override def speak() = println("Meow people")
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
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 usual 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.*")
end StringUtils
Extension methods are discussed in the [Extension methods section][extension] of the Contextual Abstraction chapter.
Their main purpose is to let you add new functionality to closed classes.
As shown in that section, imagine that you have a Circle
class, but you can’t change its source code.
For instance, it may 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:
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
Now when you have a Circle
instance named aCircle
, you can call those methods like this:
aCircle.circumference
aCircle.diameter
aCircle.area
See the [Extension methods section][reference_extension_methods] of this book, and the [“Extension methods” Reference page][reference] for more details.
[extension]: {% link _overviews/scala3-book/ca-extension-methods.md %} [reference_extension_methods]: {{ site.scala3ref }}/contextual/extension-methods.html [reference]: {{ site.scala3ref }}/overview.html [reference_matchable]: {{ site.scala3ref }}/other-new-features/matchable.html