Kotlin is `fun` - Some cool stuff about Kotlin functions

Lucy Linder - Mar 6 '23 - - Dev Community

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() }
Enter fullscreen mode Exit fullscreen mode

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.

🔖 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
Enter fullscreen mode Exit fullscreen mode

Or:

fun ternary(ifTrue: String, ifFalse: String, test: Boolean): String {
    return if (test) ifTrue else ifFalse
}

ternary("yes", "no", 2 == 1) // => "yes"
Enter fullscreen mode Exit fullscreen mode

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 = ' ',
) { /*...*/ }
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Can be turned into an elegant single-line declaration:

fun mod2(x: Int): Int = x % 2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

I usually abuse this feature, as chaining lets you write many (any ?) complex logics into a single statement in Kotlin.

Here is one example from goodreads-metadata-fetcher, where I extract a specific text from an HTML document and convert it to int:

fun getNumberOfPages(doc: Document): Int? =
    doc.getElementById("details")
        ?.getElementsByAttribute("itemprop")
        ?.find { it.attr("itemprop") == "numberOfPages" }
        ?.text()?.split(" ")?.first()
        ?.toInt()
Enter fullscreen mode Exit fullscreen mode

(the ?. 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)
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .