Lesson 06 / 23
Functions
Introduces parameters with default values, named arguments, and the `vararg` parameter; measures that a function reference gives a separate but equal object each time it is taken.
Published Updated
In this lesson
- Declares a function with `fun` and uses parameters with default values, named arguments, and `vararg`
- Explains that a default value is computed again at every call and what this does to the equality criterion
- Measures that taking a function reference gives separate but equal objects
Contents
In control flow we saw how to distribute a value to branches and how to walk over a range. In this lesson we package that flow into named, reusable pieces: functions. First we look at how the parameters of a function can be made flexible, then we ask the same question of the functions themselves: when we take the same function twice as a value, do we get two equal objects, or a single object?
Making Parameters Flexible
In Kotlin, a function is declared with the fun keyword: the name, then the parameters inside parentheses in the form name: Type, then a colon and the return type. If the body is a single expression, it can be written with =. Three features make the parameter list flexible.
The first is the default value: if = value is added to a parameter, the caller can leave it out. The second is the named argument: if punctuation = "?" is written in the call, the value is bound by name, not by position, and the parameters in between stay at their defaults. The third is the vararg parameter: if a parameter is marked vararg, the caller can give as many values as they want, and inside the function these appear as an array. To pass an array you already have to a vararg parameter, a * is placed in front of the array.
In this lesson too, the measurements are made with the helper file introduced in the first lesson. The Criterion class takes a value, its twin, and a different value, and asks three questions; the file is given below exactly as it was.
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
val sameObject = first === twin
val equalTwin = first == twin
val equalOther = first == other
val name: String = when {
sameObject -> "singleton"
!equalTwin -> "identity"
!equalOther -> "content"
else -> "partial-content"
}
fun report() {
println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
}
}
fun summarize(checks: List<Criterion>) {
for (name in listOf("singleton", "identity", "content", "partial-content")) {
println("$name: ${checks.count { it.name == name }}")
}
}
A default value does not have to be a constant; it can be any expression. When the caller leaves the parameter out, that expression is computed at the moment of the call, so two calls of the same function can get two separate results from the default. A named argument is also not only a convenience of order: someone reading the call understands what the value belongs to from the writing punctuation = "?", without looking at the declaration of the function. In the second section we will measure this behavior of the default value.
The program below uses the three features together. greet carries two parameters with default values, and sum takes a vararg.
// Main.kt — default value, named argument, and vararg
fun greet(name: String, greeting: String = "Hello", punctuation: String = "."): String =
"$greeting, $name$punctuation"
fun sum(vararg numbers: Int): Int = numbers.sum()
fun main() {
println(greet("Ada"))
println(greet("Ada", punctuation = "?"))
println(greet(greeting = "Hi", name = "Sam"))
println(sum())
println(sum(1, 2, 3))
val values = intArrayOf(4, 5)
println(sum(*values, 6))
}
Hello, Ada. Hello, Ada? Hi, Sam. 0 6 15
The first two lines show calls in which the name is given alone and in which the punctuation is changed with a named argument: when greeting and punctuation were left out, Hello and the period were used, and when punctuation = "?" was written, only the punctuation changed. In the third line the arguments were given out of order, by their names, and Hi, Sam. came out. The last three lines are for vararg: when no value was given, the total was 0, and with three values it was 6; sum(*values, 6) added the two elements of the array and the extra six together and gave 15.
LB38. Default values and vararg were measured only with String and Int parameters; the same behavior for parameters whose type is a class was not tested separately in this lesson.
LB39. Reordering with named arguments was shown in only one call; the rules for mixing named and positional arguments in the same call are outside this lesson.
Function Reference and Default Value
The measurement of the lesson looks at two things. The first is the function reference: the writing ::plain gives the plain function as a value without calling it. Does using the same writing twice give the same object, or separate objects? The second is the default value: does the default into: MutableList<Int> = mutableListOf() build a new list at every call, or share a single list?
collectMutable adds an item to the list it receives and returns the list; if no list is given, the default empty mutable list is used. defaultList returns the listOf() default directly. We write three observations: function-reference, two ::plain and, as the different value, ::other; default-mutableListOf, collectMutable(1) twice and collectMutable(2) as the different value; default-listOf, defaultList() twice and listOf(1) as the different value.
// Main.kt — function reference and default values
fun plain(n: Int): Int = n
fun other(n: Int): Int = n + 1
fun collectMutable(item: Int, into: MutableList<Int> = mutableListOf()): MutableList<Int> {
into.add(item)
return into
}
fun defaultList(into: List<Int> = listOf()): List<Int> = into
fun main() {
val checks = listOf(
Criterion("function-reference", ::plain, ::plain, ::other),
Criterion("default-mutableListOf", collectMutable(1), collectMutable(1), collectMutable(2)),
Criterion("default-listOf", defaultList(), defaultList(), listOf(1)),
)
for (check in checks) {
check.report()
}
summarize(checks)
}
function-reference: same-object=false, twin=true, other=false -> content default-mutableListOf: same-object=false, twin=true, other=false -> content default-listOf: same-object=true, twin=true, other=false -> singleton singleton: 1 identity: 0 content: 2 partial-content: 0
The function-reference line shows same-object=false and twin=true: the two ::plain writings gave separate objects, but these two objects came out equal by ==. Separate objects, equal content: the content criterion. default-mutableListOf is also content. The two calls built two separate lists, and since both carry [1], they are equal. The default value is computed again every time the function is called; an item added in one call does not leak into the next call.
default-listOf, on the other hand, is a singleton: same-object=true. The empty listOf() call gave the same object both times. The summary lines show one singleton, zero identity, two content, and zero partial content.
LB40. The equality of function references was measured only with top-level functions; how a reference bound to an object (object::name) is made equal was not measured in this lesson.
LB41. That the empty listOf() call gives a single object was measured on the JVM target of Kotlin 2.4.21; since this behavior depends on the implementation of the list, the same is not claimed for other versions.
The Default Value Is Computed at Every Call
We saw that a default value can be an expression. The real question is when the expression runs: once, when the function is defined, or at every call? The block below takes the default value from a function that increments a counter, and also tries whether the default of a parameter can depend on the one before it.
// Main.kt — the default value is computed at every call
var counter = 0
fun nextId(): Int {
counter++
return counter
}
fun label(text: String, id: Int = nextId(), width: Int = text.length): String =
"$text#$id/$width"
fun main() {
println(label("ab"))
println(label("abc"))
println(label("abc", id = 100))
println(label("abc", width = 1))
println(counter)
}
ab#1/2 abc#2/3 abc#100/3 abc#3/1 3
The first call gave ab#1/2: nextId() ran for id and produced 1, and since the width default is text.length, it became 2. The second call gave abc#2/3; the counter was incremented a second time. So the default value is computed again at every call. In the third call id was given explicitly as 100 and abc#100/3 was printed; in this case nextId() did not run at all. The fourth call gave abc#3/1: width became 1 through the named argument, the counter ran again for id, and produced 3. The last line gave the value of counter as 3: of the four calls, only three used the default id.
This proves two things. First, the default expression runs at the moment of the call; if it has a side effect, it repeats at every call. Second, defaults can reach the parameters before them (width looks at text). The frequent mistake is to assume that the default value is frozen at definition; in Python, the evaluation of a mutable default value once at definition is a known trap, and there is no such trap in Kotlin. Java has no default parameters; the same effect is obtained with overloading.
LB42. The default value measurement was made with a top-level counter; the behavior of the counter in a multi-threaded environment was not measured in this lesson.
Unit, Nothing, and a Local Function
A function whose return value is not written actually returns something. The block below prints a function that returns Unit, catches a function that returns Nothing inside a try, and uses a function placed inside another function.
// Main.kt — Unit, Nothing, and a local function
fun nothingToReturn() {
}
fun fail(message: String): Nothing = throw IllegalStateException(message)
fun total(values: List<Int>): Int {
fun square(n: Int) = n * n
var sum = 0
for (v in values) {
sum += square(v)
}
return sum
}
fun main() {
println(nothingToReturn())
println(nothingToReturn() === Unit)
println(total(listOf(1, 2, 3)))
val result = try {
fail("stopped")
} catch (e: IllegalStateException) {
e.message
}
println(result)
}
kotlin.Unit true 14 stopped
The first line gave kotlin.Unit: the function that we thought returned nothing actually returns the singleton object named Unit, and println can print it. The true in the second line shows that the comparison nothingToReturn() === Unit is true: Unit is a single object. This is a basic difference from void in Java; void is not a value, while Unit is a real value, and for this reason Unit can be used in generic types.
The third line is 14, that is, the sum of 1, 4, and 9. The square function is defined inside total and can be called only from there. A local function can reach the variables of the outer function; this was not used here and the access was not measured.
The last line gave stopped. The return type of the fail function is Nothing: it never returns normally, it always throws an exception. The compiler knows this; since the fail(...) call had to produce a value inside try, the message of the caught exception became the result. The frequent mistake is to confuse the Nothing type with Unit. Unit returns and execution continues, while Nothing never returns.
LB43. The Nothing measurement was made with a function that throws an exception; marking functions that call an infinite loop or exitProcess with Nothing was not measured in this lesson.
Overload Selection and an Extension Function
Functions that carry the same name are chosen according to the parameter type. The block below defines four show functions and looks at which one is chosen with five different calls. Then it writes extension functions on an Int and a String, and shows how a MutableList parameter affects the calling side.
// Main.kt — overload selection, extension function, and parameter
fun show(x: Int) = "Int"
fun show(x: Long) = "Long"
fun show(x: Any) = "Any"
fun show(x: String) = "String"
fun Int.isEven(): Boolean = this % 2 == 0
fun String.initials(): String = split(" ").joinToString("") { it.take(1).uppercase() }
fun modify(list: MutableList<Int>) {
list.add(99)
}
fun main() {
println(show(1))
println(show(1L))
println(show("a"))
val any: Any = "a"
println(show(any))
println(show(1.5))
println(4.isEven())
println("ada lovelace".initials())
val nums = mutableListOf(1)
modify(nums)
println(nums)
}
Int Long String Any Any true AL [1, 99]
show(1) gave Int, show(1L) gave Long, and show("a") gave String: the compiler chooses the most specific match. The fourth line is interesting. The any variable carries a String at run time, but because its type was declared as Any, show(any) chose the Any version. Overload resolution is made at compile time, according to the declared type of the variable; the run-time type does not affect the choice. In Java too, overloading is resolved at compile time, while a virtual method call is resolved according to the run-time type, and the two must not be confused. show(1.5) gave Any, because it fits only the Any version.
4.isEven() gave true and "ada lovelace".initials() gave AL: an extension function adds a new form of call to an existing type without changing the type itself. An extension function is resolved statically; it is not a real member of a class.
The last line gave [1, 99]. The modify function added to the list it received as a parameter, and the calling side saw this. In Kotlin, the reference itself is copied as a value: the object is shared, but the function cannot bind the caller’s variable to another object; the parameter variable cannot be reassigned anyway. The frequent mistake is to make a change inside a collection passed to a function and assume that it will stay local.
LB44. Overload selection was measured with four definitions and five calls; which version Short or Byte arguments go to, besides Int and Long, was not measured in this lesson.
Habits That Come from the Measurements
The first thing the measurements show in a function definition is that the default value is an expression and runs again at every call. This removes the mutable-default trap of Python in Kotlin, but it creates another risk: if the default expression has a side effect, the number of calls affects the behavior of the program. In the measurement the counter value became three after four calls, because the default value was used in only three calls. Keeping default expressions free of side effects is a good habit for this reason; work such as incrementing a counter should be done in the body as an explicit call.
The distinction between Unit and Nothing is a useful part of the type system. We measured that a function returning Unit returns a singleton value; for this reason Unit is used in generic types without trouble. Nothing is the type of an expression that never returns, and in this way a writing such as val x = y ?: fail("missing") keeps x in the non-null type. The compiler knows that the code after the fail function will not run, so it can give an unreachable-code warning.
In overloading, the choice is made at compile time and looks at the declared type. Although the any variable carried a String, the Any version was chosen. This behavior is different from a virtual method call being resolved according to the run-time type, and code that confuses the two calls an unexpected version. As an overload set grows, it becomes harder to follow which call goes to which version; in that case separately named functions (showInt, showLong) can be more readable.
Extension functions add a form of call without changing the type. When you want to add a method to a library class, writing an extension is often enough instead of creating a subclass. But an extension is not a real member; it cannot reach the private fields of the class and is resolved statically, that is, it is not chosen according to the run-time type.
A mutable collection passed as a parameter changes on the calling side as well. If you do not want this, take the function parameter in the read-only List type or make a copy of it. The [1, 99] output in the measurement is a short proof of this trap.
The Bounding Measurement
The content criterion holds for the function reference, but one should not read the fact that the reference is a separate object each time it is taken as a contract: what was measured is that the two takings come out equal by ==. In the same lesson the listOf() default came out singleton. That is, the writing of a default value alone does not determine the criterion: mutableListOf() is content because it builds a new object at every call, and listOf() is a singleton because it returns the same empty list. When looking at the default of a parameter, the criterion depends on what that expression builds.
Summary
- In a function declared with
fun, parameters with default values, named arguments, andvarargmake the call flexible;*spreads an array into avarargparameter. - The default value is computed again at every call; the
mutableListOf()default is not shared between calls. - Two function references such as
::plainare separate objects but equal by==: content. - The empty
listOf()default gave the same object in two calls: singleton. - This lesson’s criterion: content; bounding measurement: the
listOf()default is a singleton, and themutableListOf()default is content.
Next Step
We have seen how to take and pass functions as a value. In the next lesson we move on to writing a function in place: lambdas and higher-order functions. There we ask the same question for two lambdas: are two lambdas the same object, equal, or neither?
To keep your progress and take notes, Log in
My notes
Log in to take notes.