About the Series
Let's deep dive into Kotlin functions, from the simple theory to the more advanced concepts of extension functions and lambda receivers. After reading this series, constructs such as the following should not scare you off anymore:
fun <T, R, S> x(a: (T) -> R, b: R.() -> S): (T) -> S =
{ t: T -> a(t).b() }
The only thing you need is a basic knowledge of Kotlin - I won't explain the 101. Let's get started!
You got me, I am a big fan of Kotlin. Some of the (many) things I love about it are its functional programming support, its compact syntax, and the advanced yet very handy concepts of extension functions, delegates, and more.
Since I won't be able to cover everything offered by the language, let's restrict this (first?) Series to Kotlin functions, starting with some cool stuff about them.
- A quick reminder (you can skip it)
- Optional named arguments
- Single expression functions
- Local functions
🔖 I created this Table of Contents using BitDownToc. If you are curious, read my article: Finally a clean and easy way to add Table of Contents to dev.to articles 🤩
A quick reminder (you can skip it)
For those catching up, a function in Kotlin is declared using the fun
parameter, a function name, optional arguments in parenthesis written <name>: <type>
, followed by a :
and an optional return type.
For example:
fun printHello() {
println("Hello, World!")
}
printHello() // => writes the string to stdout
Or:
fun ternary(ifTrue: String, ifFalse: String, test: Boolean): String {
return if (test) ifTrue else ifFalse
}
ternary("yes", "no", 2 == 1) // => "yes"
Functions can, of course, have modifiers (internal
, private
, inline
, etc), but no static
! "Static" methods are methods declared in object
.
Optional named arguments
Kotlin supports both optional (with defaults) and named arguments, which removes most of the need for the ugly function overloading pattern (java).
Take this function:
fun reformat(
str: String,
normalize: Boolean = true,
useTitleCase: Boolean = true,
wordSeparator: Char = ' ',
) { /*...*/ }
It declares one required argument of type String
, and multiple optional configuration parameters. Note that optional arguments always come after required ones.
All the following calls are perfectly valid:
// ↓↓ preferred
reformat("use defaults")
reformat("change useTitleCase only", useTitleCase = false)
// ↓↓ ugly, but works!
reformat(str = "use defaults")
reformat(useTitleCase = false, str = "changing order")
reformat("all params no names", false, true, '-')
reformat(
"mix",
false,
useTitleCase = false,
'_' // if all params are present, we can omit the name
)
So optional named arguments let the caller pick which parameters he wants to specify, leaving the rest to the default implementation. While a lot of syntaxes are allowed, I would advise you to:
- use the same order in the call as in the declaration
- always use named arguments for optional parameters
- prefer named arguments when there are many parameters or multiple parameters of the same type.
This works the same way for constructors, which are also functions
data class Person(name: String, hasPet: Boolean = false)
Person("Georges")
Person("Wallace", hasPet = true)
Single expression functions
When a function's body is only a single expression, one can omit the curly braces and the return
by using =
.
Hence, this:
fun mod2(x: Int): Int {
return x % 2
}
Can be turned into an elegant single-line declaration:
fun mod2(x: Int): Int = x % 2
Kotlin type inference is very good, so in this case, you can even omit the return type (though it shouldn't be taken as a good practice):
fun mod2(x: Int) = x % 2
Remember, if/else
and try/catch
are expressions too! So this also is valid:
fun trySomething() = try {
/*...*/
} catch {
/*...*/
}
fun conditional() = if (something) a() else b()
Here is one example from goodreads-metadata-fetcher, where I extract a specific text from an HTML document and convert it to int: (the I usually abuse this feature, as chaining lets you write many (any ?) complex logics into a single statement in Kotlin.
fun getNumberOfPages(doc: Document): Int? =
doc.getElementById("details")
?.getElementsByAttribute("itemprop")
?.find { it.attr("itemprop") == "numberOfPages" }
?.text()?.split(" ")?.first()
?.toInt()
?.
is the safe call operator).
Local functions
Functions can be defined inside other functions. In this case, the inner function is only available from inside the outer function and can access the local variables of the outer scope.This is called a closure.
Here is a good example of a local function:
fun fuzzyCompare(expected: String, actual: String): Boolean {
fun clean(s: String) = s
.lowercase()
.replace("[^a-z0-9]".toRegex(), "")
.replace("\\s+".toRegex(), " ")
.trim()
return clean(actual) == clean(expected)
}
The clean
function here is very specific to the fuzzy compare functionality. Instead of making it a private utility function in the same file, we can directly encapsulate it inside the fuzzy compare function itself, making the code both clear and clean.
Even better, Kotlin offers mutable closures! Thus, contrary to Java, it is possible to mutate an outer variable from inside a local function (or a lambda). Here is an example:
fun sumList(list: List<Int>): List<Int> {
var sum = 0
fun add(i: Int): Int {
sum += i; return sum
}
return list.map { add(it) }
}
println(sumList(listOf(1, 2, 3))) // [1,3,6]
Note: this is a very bad example, as the same can be achieved using functional programming built-ins:
list.runningFold(0, Int::plus).drop(1)
If you are interested in how local functions are implemented under the hood (or their cost), see Idiomatic Kotlin: Local functions from Tompee Balauag.
That's it for this first article, which only scraped the surface. The next one will be more theoretical, giving you the conceptual overview necessary to truly understand Kotlin functions, higher-order functions, and lambdas. I will try to keep it light and fun
though, I promise. Stay tuned!