Kotlin Bootcamp for Programmers 6: Functional manipulation

1. Welcome

This codelab is part of the Kotlin Bootcamp for Programmers course. You'll get the most value out of this course if you work through the codelabs in sequence. Depending on your knowledge, you may be able to skim some sections. This course is geared towards programmers who know an object-oriented language, and want to learn Kotlin.

sEioGm-YJlcEfGjX0S6M-MQDi23k2ZjQCNPkuImT4e5BIqCJ7XCoLqvDJlUK4cB9XfffJQOcpcW_I8J1LRpYN6qk_b7NMWSQi_0yAWk6Gm5e9C-vvNo5v8geG9iINqKPc_byPxgqMA

Introduction

This is the final codelab in the Kotlin Bootcamp. In this codelab you learn about annotations and labeled breaks. You review lambdas and higher- order functions, which are key parts of Kotlin. You also learn more about inlining functions, and Single Abstract Method (SAM) interfaces. Finally, you learn more about the Kotlin Standard Library.

Rather than build a single sample app, the lessons in this course are designed to build your knowledge, but be semi-independent of each other so you can skim sections you're familiar with. To tie them together, many of the examples use an aquarium theme. And if you want to see the full aquarium story, check out the Kotlin Bootcamp for Programmers Udacity course.

BLgezynJ_92kR7lbZPbmkh7cDUCFMm3Ugo_JUOdDd5IpMdkk8nu3nbMiSkQWK5dx4-NX4qlbUXwU9l_Pj_7QqoRSUX2YbiddIUO9I100elofv-IY6xAHo7RL9CCXnjEwBKyLknPHzw

What you should already know

  • The syntax of Kotlin functions, classes, and methods
  • How to create a new class in the IntelliJ IDEA and run a program
  • The basics of lambdas and higher-order functions

What you'll learn

  • The basics of annotations
  • How to use labeled breaks
  • More about higher-order functions
  • About Single Abstract Method (SAM) interfaces
  • About the Kotlin Standard Library

What you'll do

  • Create a simple annotation.
  • Use a labeled break.
  • Review lambda functions in Kotlin.
  • Use and create higher-order functions.
  • Call some Single Abstract Method interfaces.
  • Use some functions from the Kotlin Standard Library.

2. Task: Learn about annotations

Annotations are a way of attaching metadata to code, and are not something specific to Kotlin. The annotations are read by the compiler and used to generate code or logic. Many frameworks, such as Ktor and Kotlinx, as well as Room, use annotations to configure how they run and interact with your code. You are unlikely to encounter any annotations until you start using frameworks, but it's useful to be able to know how to read an annotation.

There are also annotations that are available through the Kotlin standard library that control the way code is compiled. They're really useful if you're exporting Kotlin to Java code, but otherwise you don't need them that often.

Annotations go right before the thing that is annotated, and most things can be annotated—classes, functions, methods, and even control structures. Some annotations can take arguments.

Here is an example of some annotations.

@file:JvmName("InteropFish")
class InteropFish {
   companion object {
       @JvmStatic fun interop()
   }
}

This says the exported name of this file is InteropFish with the JvmName annotation; the JvmName annotation is taking an argument of "InteropFish". In the companion object, @JvmStatic tells Kotlin to make interop() a static function in InteropFish.

You can also create your own annotations, but this is mostly useful if you are writing a library that needs particular information about classes at runtime, that is reflection.

Step 1: Create a new package and file

  1. Under src, create a new package, example.
  2. In example, create a new Kotlin file, Annotations.kt.

Step 2: Create your own annotation

  1. In Annotations.kt, create a Plant class with two methods, trim() and fertilize().
class Plant {
        fun trim(){}
        fun fertilize(){}
}
  1. Create a function that prints all the methods in a class. Use ::class to get information about a class at runtime. Use declaredMemberFunctions to get a list of the methods of a class. (To access this, you need to import kotlin.reflect.full.*)
import kotlin.reflect.full.*    // required import

class Plant {
    fun trim(){}
    fun fertilize(){}
}

fun testAnnotations() {
    val classObj = Plant::class
    for (m in classObj.declaredMemberFunctions) {
        println(m.name)
    }
}
  1. Create a main() function to call your test routine. Run your program and observe the output.
fun main() {
    testAnnotations()
}
⇒ trim
fertilize
  1. Create a simple annotation, ImAPlant.
annotation class ImAPlant

This doesn't do anything other than say it is annotated.

  1. Add the annotation in front of your Plant class.
@ImAPlant class Plant{
    ...
}
  1. Change testAnnotations() to print all the annotations of a class. Use annotations to get all the annotations of a class. Run your program and observe the result.
fun testAnnotations() {
    val plantObject = Plant::class
    for (a in plantObject.annotations) {
        println(a.annotationClass.simpleName)
    }
}
⇒ ImAPlant
  1. Change testAnnotations() to find the ImAPlant annotation. Use findAnnotation() to find a specific annotation. Run your program and observe the result.
fun testAnnotations() {
    val plantObject = Plant::class
    val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
    println(myAnnotationObject)
}
⇒ @example.ImAPlant()

Step 3: Create a targeted annotation

Annotations can target getters or setters. When they do, you can apply them with the @get: or @set: prefix. This comes up a lot when using frameworks with annotations.

  1. Declare two annotations, OnGet which can only be applied to property getters, and OnSet which can only be applied to property setters. Use @Target(AnnotationTarget.PROPERTY_GETTER) or PROPERTY_SETTER on each.
annotation class ImAPlant

@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class OnGet
@Target(AnnotationTarget.PROPERTY_SETTER)
annotation class OnSet

@ImAPlant class Plant {
    @get:OnGet
    val isGrowing: Boolean = true

    @set:OnSet
    var needsFood: Boolean = false
}

Annotations are really powerful for creating libraries that inspect things both at runtime and sometimes at compile time. However, typical application code just uses annotations provided by frameworks.

3. Task: Learn about labeled breaks

Kotlin has several ways of controlling flow. You are already familiar with return, which returns from a function to its enclosing function. Using a break is like return, but for loops.

Kotlin gives you additional control over loops with what's called a labeled break. A break qualified with a label jumps to the execution point right after the loop marked with that label. This is particularly useful when dealing with nested loops.

Any expression in Kotlin may be marked with a label. Labels have the form of an identifier followed by the @ sign.

  1. In Annotations.kt, try out a labeled break by breaking out from an inner loop.
fun labels() {
    outerLoop@ for (i in 1..100) {
         print("$i ")
         for (j in 1..100) {
             if (i > 10) break@outerLoop  // breaks to outer loop
        }
    }
}

fun main() {
    labels()
}
  1. Run your program and observe the output.
⇒ 1 2 3 4 5 6 7 8 9 10 11 

Similarly, you can use a labeled continue. Instead of breaking out of the labeled loop, the labeled continue proceeds to the next iteration of the loop.

4. Task: Create simple lambdas

Lambdas are anonymous functions, which are functions with no name. You can assign them to variables and pass them as arguments to functions and methods. They are extremely useful.

Step 1: Create a simple lambda

  1. Start the REPL in IntelliJ IDEA, Tools > Kotlin > Kotlin REPL.
  2. Create a lambda with an argument, dirty: Int that does a calculation, dividing dirty by 2. Assign the lambda to a variable, waterFilter.
val waterFilter = { dirty: Int -> dirty / 2 }
  1. Call waterFilter, passing a value of 30.
waterFilter(30)
⇒ res0: kotlin.Int = 15

Step 2: Create a filter lambda

  1. Still in the REPL, create a data class, Fish, with one property, name.
data class Fish(val name: String)
  1. Create a list of 3 Fish, with names Flipper, Moby Dick, and Dory.
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))
  1. Add a filter to check for names that contain the letter ‘i'.
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]

In the lambda expression, it refers to the current list element, and the filter is applied to each list element in turn.

  1. Apply joinString() to the result, using ", " as the separator.
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick

The joinToString() function creates a string by joining the filtered names, separated by the string specified. It is one of the many useful functions built into the Kotlin standard library.

5. Task: Write a higher-order function

Passing a lambda or other function as an argument to a function creates a higher-order function. The filter above is a simple example of this. filter() is a function, and you pass it a lambda that specifies how to process each element of the list.

Writing higher-order functions with extension lambdas is one of the most advanced parts of the Kotlin language. It takes a while to learn how to write them, but they are really convenient to use.

Step 1: Create a new class

  1. Within the example package, create a new Kotlin file, Fish.kt.
  2. In Fish.kt, create a data class Fish, with one property, name.
data class Fish (var name: String)
  1. Create a function fishExamples(). In fishExamples(), create a fish named "splashy", all lowercase.
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
}
  1. Create a main() function which calls fishExamples().
fun main () {
    fishExamples()
}
  1. Compile and run your program by clicking the green triangle to the left of main(). There is no output yet.

Step 2: Use a higher-order function

The with() function lets you make one or more references to an object or property in a more compact way. Using this. with() is actually a higher-order function, and in the lamba you specify what to do with the supplied object.

  1. Use with() to capitalize the fish name in fishExamples(). Within the curly braces, this refers to the object passed to with().
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        this.capitalize()
    }
}
  1. There is no output, so add a println() around it. And the this is implicit and not needed, so you can remove it.
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        println(capitalize())
    }
}
⇒ Splashy

Step 3: Create a higher-order function

Under the hood, with() is a higher-order function. To see how this works, you can make your own greatly simplified version of with() that just works for strings.

  1. In Fish.kt, define a function, myWith() that takes two arguments. The arguments are the object to operate on, and a function that defines the operation. The convention for the argument name with the function is block. In this case, that function returns nothing, which is specified with Unit.
fun myWith(name: String, block: String.() -> Unit) {}

Inside myWith(), block() is now an extension function of String. The class being extended is often called the receiver object. So name is the receiver object in this case.

  1. In the body of myWith(), apply the passed in function, block(), to the receiver object, name.
fun myWith(name: String, block: String.() -> Unit) {
    name.block()
}
  1. In fishExamples(), replace with() with myWith().
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    myWith (fish.name) {
        println(capitalize())
    }
}

fish.name is the name argument, and println(capitalize()) is the block function.

  1. Run the program, and it operates as before.
⇒ Splashy

Step 4: Explore more built in extensions

The with() extension lambda is very useful, and is part of the Kotlin Standard Library. Here are a few of the others you might find handy: run(), apply(), and let().

The run() function is an extension that works with all types. It takes one lambda as its argument and returns the result of executing the lambda.

  1. In fishExamples(), call run() on fish to get the name.
fish.run {
   name
}

This just returns the name property. You could assign that to a variable or print it. This isn't actually a useful example, as you could just access the property, but run() can be useful for more complicated expressions.

The apply() function is similar to run(), but it returns the changed object it was applied to instead of the result of the lambda. This can be useful for calling methods on a newly created object.

  1. Make a copy of fish and call apply() to set the name of the new copy.
val fish2 = Fish(name = "splashy").apply {
     name = "sharky"
}
println(fish2.name)
⇒ sharky

The let() function is similar to apply(), but it returns a copy of the object with the changes. This can be useful for chaining manipulations together.

  1. Use let() to get the name of fish, capitalize it, concatenate another string to it, get the length of that result, add 31 to the length, then print the result.
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
⇒ 42

In this example, the object type referred to by it is Fish, then String, then String again and finally Int.

  1. Print fish after calling let(), and you will see that it hasn't changed.
println(fish.let { it.name.capitalize()}
    .let{it + "fish"}
    .let{it.length}
    .let{it + 31})
println(fish)
⇒ 42
Fish(name=splashy)

6. Concept: Inline functions

Lambdas and higher-order functions are really useful, but there is something you should know: lambdas are objects. A lambda expression is an instance of a Function interface, which is itself a subtype of Object. Consider the earlier example of myWith().

myWith(fish.name) {
    capitalize()
}

The Function interface has a method, invoke(), which is overridden to call the lambda expression. Written out longhand, it would look something like the code below.

// actually creates an object that looks like this
myWith(fish.name, object : Function1<String, Unit> {
    override fun invoke(name: String) {
        name.capitalize()
    }
})

Normally this isn't a problem, because creating objects and calling functions doesn't incur much overhead, that is, memory and CPU time. But if you're defining something like myWith() that you use everywhere, the overhead could add up.

Kotlin provides inline as a way to handle this case to reduce overhead during runtime by adding a bit more work for the compiler. (You learned a little about inline in the earlier lesson talking about reified types.) Marking a function as inline means that every time the function is called, the compiler will actually transform the source code to "inline" the function. That is, the compiler will change the code to replace the lambda with the instructions inside the lambda.

If myWith() in the above example is marked with inline:

inline myWith(fish.name) {
    capitalize()
}

it is transformed into a direct call:

// with myWith() inline, this becomes
fish.name.capitalize()

It is worth noting that inlining large functions does increase your code size, so it's best used for simple functions that are used many times like myWith(). The extension functions from the libraries you learned about earlier are marked inline, so you don't have to worry about extra objects being created.

7. Task: Learn about Single Abstract Methods

Single Abstract Method just means an interface with one method on it. They are very common when using APIs written in the Java programming language, so there is an acronym for it, SAM. Some examples are Runnable, which has a single abstract method, run(), and Callable, which has a single abstract method, call().

In Kotlin, you have to call functions that take SAMs as parameters all the time. Try the example below.

  1. Inside example, create a Java class, JavaRun, and paste the following into the file.
package example;

public class JavaRun {
    public static void runNow(Runnable runnable) {
        runnable.run();
    }
}

Kotlin lets you instantiate an object that implements an interface by preceding the type with object:. It's useful for passing parameters to SAMs.

  1. Back in Fish.kt, create a function runExample(), which creates a Runnable using object: The object should implement run() by printing "I'm a Runnable".
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
}
  1. Call JavaRun.runNow() with the object you created.
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
    JavaRun.runNow(runnable)
}
  1. Call runExample() from main() and run the program.
⇒ I'm a Runnable

A lot of work to print something, but a good example of how a SAM works. Of course, Kotlin provides a simpler way to do this—use a lambda in place of the object to make this code a lot more compact.

  1. Remove the existing code in runExample, change it to call runNow() with a lambda, and run the program.
fun runExample() {
    JavaRun.runNow({
        println("Passing a lambda as a Runnable")
    })
}
⇒ Passing a lambda as a Runnable
  1. You can make this even more concise using the last parameter call syntax, and get rid of the parentheses.
fun runExample() {
    JavaRun.runNow {
        println("Last parameter is a lambda as a Runnable")
    }
}
⇒ Last parameter is a lambda as a Runnable

That's the basics of a SAM, a Single Abstract Method. You can instantiate, override and make a call to a SAM with one line of code, using the pattern: Class.singleAbstractMethod { lambda_of_override }

8. Summary

This lesson reviewed lambdas and went into more depth with higher-order functions—key parts of Kotlin. You also learned about annotations and labeled breaks.

  • Use annotations to specify things to the compiler. For example: @file:JvmName("Foo")
  • Use labeled breaks to let your code exit from inside nested loops. For example: if (i > 10) break@outerLoop // breaks to outerLoop label
  • Lambdas can be very powerful when coupled with higher-order functions.
  • Lambdas are objects. To avoid creating the object, you can mark the function with inline, and the compiler will put the contents of the lambda in the code directly.
  • Use inline carefully, but it can help reduce resource usage by your program.
  • SAM, Single Abstract Method, is a common pattern, and made simpler with lambdas. The basic pattern is: Class.singleAbstractMethod { lamba_of_override }
  • The Kotlin Standard Library provides numerous useful functions, including several SAMs, so get to know what's in it.

There's lots more to Kotlin than was covered in the course, but you now have the basics to begin developing your own Kotlin programs. Hopefully you're excited about this expressive language, and looking forward to creating more functionality while writing less code (especially if you're coming from the Java programming language.) Practice and learning as you go is the best way to become an expert in Kotlin, so continue to explore and learn about Kotlin on your own.

9. Learn more

Kotlin documentation

If you want more information on any topic in this course, or if you get stuck, https://kotlinlang.org is your best starting point.

Kotlin tutorials

The https://play.kotlinlang.org website includes rich tutorials called Kotlin Koans, a web-based interpreter, and a complete set of reference documentation with examples.

Udacity course

To view the Udacity course on this topic, see Kotlin Bootcamp for Programmers.

IntelliJ IDEA

Documentation for the IntelliJ IDEA can be found on the JetBrains website.

Kotlin Standard Library

The Kotlin Standard Library provides numerous useful functions. Before you write your own function or interface, always check the Standard Library to see if someone has saved you some work. Check back occasionally, because new functionality is added frequently.

Kotlin tutorials

The https://play.kotlinlang.org website includes rich tutorials called Kotlin Koans, a web-based interpreter, and a complete set of reference documentation with examples.

Udacity course

To view the Udacity course on this topic, see Kotlin Bootcamp for Programmers.

IntelliJ IDEA

Documentation for the IntelliJ IDEA can be found on the JetBrains website.

10. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

In Kotlin, SAM stands for:

▢ Safe Argument Matching

▢ Simple Access Method

▢ Single Abstract Method

▢ Strategic Access Methodology

Question 2

Which one of the following is not a Kotlin Standard Library extension function?

elvis()

apply()

run()

with()

Question 3

Which one of the following is not true of lambdas in Kotlin?

▢ Lambdas are anonymous functions.

▢ Lambdas are objects unless inlined.

▢ Lambdas are resource intensive and shouldn't be used.

▢ Lambdas can be passed to other functions.

Question 4

Labels in Kotlin are indicated with an identifier followed by:

:

::

@:

@

11. Next steps

Congratulations! You've completed the Kotlin Bootcamp for Programmers codelab.

For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."

If you're a Java programmer, you may be interested in the Refactoring to Kotlin codelab. The automated Java to Kotlin conversion tools cover the basics, but you can create more concise, robust code with a little extra work.

If you're interested in developing apps for Android, take a look at Android Kotlin Fundamentals.