In this unit, you’ll learn about functions — how to specify default values and how to use named arguments. You’ll also learn about Kotlin’s nullable values and how to specify them as function arguments, how to use smart casts to make your code more compact, how to perform numeric conversions, and lots more.

Unit objectives

In this unit, you will learn:

  • The difference between block body and expression body functions
  • How default arguments work
  • How to work with named arguments
  • What nullable values are and how to use them as function arguments
  • How smart casts work
  • How to use Kotlin’s widening and narrowing numeric conversion functions
  • The Unit type and how to use it
  • How to define and use lambda expressions

Source code for the unit

Make sure to clone the GitHub repository so you can follow along with the examples in the unit and run them for yourself.

The source code for all of the examples in this unit is in the Kotlin/Unit_7/src directory.

The source code for this unit is contained in multiple files, where each file corresponds to the section it goes with. I’ll point out which source file you should reference at the top of each section.

Function types

Source: Kotlin/Unit_7/src/FunctionTypes.kt

You declare functions with the fun keyword, as you learned in Unit 6. There are different ways to implement the logic of a function:

  • Block expression: The function’s body consists of a block of code surrounded by curly braces (that is, { and })

  • Single expression: The function’s body consists of a single expression on one line of code.

Block expression functions

This is the “normal” way to implement a function. Block expression function syntax looks like this:

fun addTwoBlock(first: Int, second: Int) : Int {
    return first + second
}

The addTwoBlock() function has two parameters: first and second, both of type Int. The function returns an Int value and the function’s logic is surrounded by curly braces.

Single expression functions

Many functions can be written with one line of code and Kotlin’s expression body function syntax makes writing these types of functions more compact.

You can rewrite the addTwoBlock() function from the previous section as a single expression function:

fun addTwoExpression(first: Int, second: Int) = first + second

Note: The body of an expression body function must be an expression and not a statement.

The function return type has been dropped because the compiler can infer the return type (Int) from the expression first + second, whose type is Int.

Default parameter values

Source: Kotlin/Unit_7/src/Default.kt

Kotlin functions allow you to specify default parameter values (or argument values or just arguments) in which you tell the compiler the value the parameter will have if you omit it.

Consider this function, which creates a java.time.LocalDateTime object:

fun createLocalDateTime(
        year: Int,
        month: Int,
        day: Int,
        hour: Int = 0,
        minute: Int = 0,
        second: Int = 0) : LocalDateTime =
    LocalDateTime.of(year, month, day, hour, minute, second)

Listing 1. The createLocalDateTime() function using default arguments

The hour, minute, and second parameters all have default parameter values of 0, which you can omit:

    // 15 Sep 2018 12:00:00
    val localDateTime = createLocalDateTime(2018, 9, 15)

Of course, you can specify all of the parameter values fully:

    // 15 Sep 2017 12:07:23
    val localDateTime = createLocalDateTime(2018, 9, 15, 12, 7, 23)

or supply only a few:

    // 15 Sep 2018 12:00:00
    val localDateTime = createLocalDateTime(2018, 9, 15, 12)

This is handy when you need to create several versions of a function, called overloads, in which each overload:

  1. Differs just slightly or

  2. Has reasonable defaults (like the way we set the default hour to 0 in the previous example)

Be sure to check out the Kotlin documentation if you want to learn more about default arguments.

Named arguments

Source: Kotlin/Unit_7/src/Named.kt

Consider the createLocalDateTime() function from Listing 1 again. Now write code to call it:

    var localDateTime = createLocalDateTime(2018, 9, 2, 12, 7, 23)

Imagine you have to revisit this code several weeks or months later. It’s sort of obvious from context that 2018 is the year value, but is that 9 the month of September and is the 2 the second day or does this code mean to create a date representing the 9th day of February? Hmm.

At this point, to be safe, you have to pull up the declaration of createLocalDateTime(), and BOOM! — there goes your productivity.

Fortunately the concept of Kotlin named parameters makes it easy to specify which parameter is which. You tell the compiler what parameter you want to go with what value using each parameter’s name, like this:

    var localDateTime = createLocalDateTime(
            year = 2018,
            month = 9,
            day = 2,
            hour = 12,
            minute = 7,
            second = 23)

Now there’s zero doubt which parameter is which and the code is self-documenting.

But wait, check this out. When you use named parameters, you can specify the parameters in whatever order you want and the compiler can handle it just fine.

Compare the following listing to the previous one:

    var localDateTime = createLocalDateTime(
            month = 9,
            day = 2,
            minute = 7,
            year = 2018,
            hour = 12,
            second = 23)

Because the parameter values include their names, this code is semantically identical to the listing before it. Even though the parameters are switched up (from the order they are declared in createLocalDateTime() I mean), the code compiles and runs just fine!

Be sure to check out the Kotlin documentation if you want to learn more about named arguments.

Nullable arguments

Source: Kotlin/Unit_7/src/Nullable.kt

The Kotlin philosophy is to avoid unchecked null values at all costs. I couldn’t agree more.

Kotlin neither allows null values to be passed as parameter values nor allows null object references unless you specify that a variable can be null.

In other words, Kotlin makes you tell the compiler “this (or that) variable can be null.” Such variables are referred to as nullable values.

Below is a function that formats a java.time.LocalDate:

fun formatLocalDate(localDate: LocalDate, formatString: String) : String {
    val dateTimeFormatter = DateTimeFormatter.ofPattern(formatString)
    return localDate.format(dateTimeFormatter)
}

This function returns a String containing the formatted date, and takes two parameters:

  1. localDate – a java.time.LocalDate object
  2. formatString – a String containing a date/time format specifier

When this function is called, neither of these parameters may be null. Ever.

Nullable variables

The designers of Kotlin know there are exceptions to this rule and that you’ll need to allow a variable to be nullable. However, when you need to make an exception, you have to explicitly declare the variable to be nullable.

Suppose you want a lenient version of the previous function that allows null values, but it also, to avoid the Java NullPointerException, makes sure the nullable parameter always has a reasonable default argument:

fun formatLocalDateLenient(localDate: LocalDate, formatString: String?) : String {
    return formatLocalDate(localDate, formatString ?: "yyyy/MM/dd")
}

You put a question mark (?) after the parameter type to declare a parameter nullable. The syntax formatString: String? says, “the parameter called formatString is of type String and is allowed to be null.”

The “Elvis” operator

The ?: between formatString and "yyyy/DD/dd" is called the Elvis operator.

The Elvis operator says, “Use formatString‘s value unless it is null, then use "yyyy/DD/dd".”

This line of code delegates to the rigid (non-nullable) function to format the LocalDate. And this is perfectly fine because even though formatLocalDateLenient() takes a nullable argument, Elvis makes sure the value passed to formatLocalDate() can never be null (thank you, thank you very much), which is in line with the Kotlin philosophy.

Taken together, along with a main() function to drive them:

fun formatLocalDateLenient(localDate: LocalDate, formatString: String?) : String {
    return formatLocalDate(localDate, formatString ?: "yyyy/MM/dd")
}

fun formatLocalDate(localDate: LocalDate, formatString: String) : String {
    val dateTimeFormatter = DateTimeFormatter.ofPattern(formatString)
    return localDate.format(dateTimeFormatter)
}

fun main(args: Array<String>) {
    var formatString: String? = null
    println("Hooray! It's: ${formatLocalDateLenient(createLocalDate(1999, 12, 31), formatString)}")
}

Be sure to check out the Kotlin documentation if you want to learn more about nullable arguments and null safety.

Smart casts

Source: Kotlin/Unit_7/src/SmartCasts.kt and Kotlin/Unit_7/src/Person.kt

Consider a function argument whose type is a base class like Any: The type of the object that is passed can be any valid subclass. In order to work with the subclass, you need to perform an operation called a cast — also known as type conversion — to the appropriate subclass type before you can do so.

Kotlin does not require casting, instead using a technique called smart casting to:

  1. Check the type and

  2. Convert the object reference to the appropriate type

Suppose you have three classes:

open class Person(val familyName: String, val givenName: String)

class Guest(familyName: String, givenName: String, val purpose: String) :
    Person(familyName, givenName)

class Employee(familyName: String, givenName: String, val employeeId: Int, val title: String) :
    Person(familyName, givenName)

Listing 2. A Person base class and two subclasses, Guest and Employee

This listing shows three classes:

  • Person, a base class for guests and employees that has two attributes: familyName and givenName

  • Guest, a Person subclass that represents a guest and has an additional purpose attribute

  • Employee, a Person subclass that represents an employee and has additional attributes: employeeId and title

The business scenario is this: Access to the company’s physical facility is granted based on the type of object (and in the case of Guest, their purpose for entering the facility). In such a scenario, you need to check the type of the object to make the determination.

Kotlin provides a binary operator for this called is:

objectReference is SomeType

where objectReference is an object reference to SomeType (such as String, Int, Person, or CrazedWombat). If objectReference‘s type is SomeType, the expression returns true, otherwise it returns false.

However, is does more than check to see if objectReference conforms to SomeType. It also casts objectReference to SomeType and it does so under the hood in a type-safe way.

Consider this example, which uses the classes from Listing 2:

fun admitEntrance(something: Any) : Boolean {
    var ret: Boolean = false
    if (something is Employee) {
        ret = true
        println("Employee access granted for ${something.title}: ${formatName(something)}.")
    } else if (something is Guest) {
        if (purposeGrantsEntry(something.purpose)) {
            ret = true
            println("Guest access granted for the purpose of ${something.purpose}: ${formatName(something)}.")
        } else {
            println("Access Denied, purpose: ${something.purpose}: ${formatName(something)}.")
        }
    } else if (something is Person) {
        println("Access Denied, ${formatName(something)}, you are but a mere Person.")
    } else {
        println("Access denied, $something, you could be anything (including not a Person)")
    }
    return ret
}

Listing 3. A function to grant (or deny) access based on the type of object is passed to the function

The admitEntrance function takes an Any object and returns a Boolean if that object should be granted access to the facility.

Notice when the something object reference is an Employee object, it is not necessary to cast it to an Employee before referencing the something.title property:

println("Employee access granted for ${something.title} : ${formatName(something)}."

If the something is Employee expression evaluates to true then the compiler makes the cast under the hood and you can use the same object reference (something in this case) to work with the actual object. Smart cast indeed.

Using the Java language, you would use the instanceof keyword, define a new variable for the Employee, cast the something object reference, and store it in the new variable, like this:

if (something instanceof Employee) {
    Employee employee = (Employee)person;
    System.out.println("Employee access granted for " +
        employee.title + ": " + formatName(employee)");
}

Be sure to check out the Kotlin documentation if you want to learn more about smart casts.

Numeric conversion functions

Source: Kotlin/Unit_7/src/NumericConversion.kt

Using the Java language, when you want to assign a 32-bit int to a 64-bit long (called a widening conversion), you just make the assignment and the details are handled automatically by the compiler.

int a = 2345;
long b = a; // compiler handles the deets automagically

Kotlin does not support automatic widening conversions. For example, the following Kotlin code will not compile:

val int: Int = 2345
val long: Long = int

Run this code in the REPL and you’ll get this error: error: type mismatch: inferred type is Int but Long was expected.

Use the toLong() method on the source object to perform the conversion:

>>> val int: Int = 2345
>>> val long = int.toLong()
>>> long
2345
>>>

All Kotlin numeric types have functions to convert from one type to another and this includes both widening and narrowing (more bits to fewer) conversions.

Consider this example which starts with a Byte and widens its value through each successively larger type all the way through Double:

    // Widening conversions
    val byte: Byte = 1

    val short: Short = byte.toShort()

    val int: Int = short.toInt()

    val long: Long = int.toLong()

    val float: Float = long.toFloat()

    val double: Double = float.toDouble()

Now consider this example of narrowing conversions:

    // Narrowing conversions truncate
    val double: Double = 1.2345E100

    val float: Float = double.toFloat()

    val long: Long = float.toLong()

    val int: Int = long.toInt()

    val short: Short = int.toShort()

    val byte: Byte = short.toByte()

Narrowing conversions can add side-effects to your code. For example, if you narrow a Kotlin source type of Long to Int and the Long‘s value is greater than will fit in an Int, the data will be truncated because Long can store much larger numbers than Int.

Use narrowing conversions with caution!

Open NumericConversion.kt in your IDE and run the main() function to see the effects of widening and narrowing conversions for yourself.

Be sure to check out the Kotlin documentation if you want to learn more about explicity numeric conversions.

The Unit type

Source: Kotlin/Unit_7/src/Unit.kt

Every Kotlin function returns a value; however, the default return type for a function is Unit, which is equivalent to “nothing useful”.

Suppose you have a function that simply prints a String to the console:

fun print(stringToPrint: String) {
    println(stringToPrint)
}

This function doesn’t need to return anything to the caller, so it returns Unit (the compiler handles this for you automatically). If you try to assign a Unit return type to this function, IntelliJ warns you that it is redundant. See Figure 1.

Redundant Unit return type

Figure 1. Specifying a return type of Unit is unnecessary since it’s the default

Kotlin’s Unit type is analogous to void in C, C++, and Java.

Check out the Kotlin documentation for Unit if you want to learn more.

Lambda expressions (lambda functions)

Source: Kotlin/Unit_7/src/Lambda.kt

The term lambda function originates from Lambda calculus and it refers to a class of functions that are anonymous and can be passed around as values.

The functions you’ve worked with so far in this unit are declared using the fun keyword. On the other hand, lambda functions, or expressions, are blocks of code that are declared like this:

[name:]([params ...]) -> return_type

where name is the optional name of the function, params are optional parameters passed to the function, and the return_type follows the arrow ->.

A simple lambda expression looks like this:

() -> LocalDate = {
    LocalDate.now()
}

This lambda expression takes no parameters (()) and returns (->) a java.time.LocalDate object when the function is invoked. Conversely, the following lambda expression takes three arguments (year, month, and day) and passes them to LocalDate.of() to create the LocalDate object.

(year: Int, month: Int, day: Int) -> LocalDate = { year, month, day ->
    LocalDate.of(year, month, day)
}

You can store a lambda expression in a variable:

val now = {
    LocalDate.now()
}

Use the following syntax to declare a function that takes a lambda expression as a parameter:

fun formatLocalDate(localDateFactory: () -> LocalDate): String {
    return localDateFactory().format(DateTimeFormatter.ofPattern("E MM/dd/yyyy"))
}

The formatLocalDate() function takes one argument: localDateFactory: () -> LocalDate, which says “A lambda expression you can refer to as localDateFactory that takes no arguments and returns a LocalDate object when invoked.”

You can use the now variable from earlier to refer to the lambda expression:

formattedLocalDate = formatLocalDate(localDateFactory = now)
println("The LocalDate is: $formattedLocalDate")

Lambda expressions are great because they are usually little blocks of code you can store in variables and pass around as function arguments.

Be sure to check out this StackOverflow article and the Kotlin doc to learn more about lambda functions.

Conclusion

In this unit, you learned:

  • The difference between block body and expression body functions
  • How default arguments work
  • How to work with named arguments
  • What nullable values are and how to use them as function arguments
  • How smart casts work
  • How to use Kotlin’s widening and narrowing numeric conversion functions
  • The Unit type and how to use it
  • How to define and use lambda expressions

Test your understanding

True or False (Explain your answer)

  1. The main benefit of using default function arguments is that it avoids passing null when a parameter does not necessarily require a value.

  2. Named arguments allow you to specify function arguments in any order you choose, provided you supply all required arguments.

    Check your answers

    1. False

    The main reason for using default arguments is to allow parameter values to be optional. The default value will be used if you don’t pass a value for that parameter.

    2. True

    When you use name = value syntax for function arguments, you must specify all required arguments, but you are free to do so in any order you like.

Short answer

Study the following code example, then answer questions 1 and 2.

val multiply: (a: Int, b: Int) -> Int = {
    a, b -> a * b
}
  1. What does this lambda expression do?

  2. What is the return type of the lambda expression?

  3. Give an example of how the Elvis operator works.

  4. What is a smart cast? Give an example.

    Check your answers

    1. It takes two arguments

    It takes two arguments: a and b, and returns their product.

    2. The return type is Int

    The return type is Int as indicated by -> Int

    3. left ?: right: evaluates to left

    The expression left ?: right: evaluates to left, unless left is null, in which case it evaluates to right.

    4. A smart cast uses the is operator to check for type

    A smart cast uses the is operator to check that an object reference (for example, a function parameter of type Any) is of a specific type (for example, String) and if the object is of that type, the compiler casts the reference to that type (String). For example:

      
      fun (objectReference: Any) {
          if (objectReference is String) {
              objectReference.trim() // Call the trim() function, which does not exist on Any because of the smart cast
          }
      }
      
      

Programming problems

  1. Write a lambda expression that takes two Int arguments (left and right) and returns their sum and a main() function to exercise the code and print the results to the console using println(). Use the following pairs of left and right arguments: (1, 3), (3, 6), (83, 134). (The function signature of main looks like fun main(args: Array<String>) { }.)

  2. Write a function called smartCast that takes a Number parameter called number and uses the smart cast operator to print out a message: "The number parameter is of type THE_TYPE", where THE_TYPE is the short name of the class (for example, Byte, Short, Int, and so forth). Make sure to include a main() function to drive the program. Make sure and supply one of each of the Numeric types (give each the value 23).

    Check your answers

    1. Here is the solution using string templates:

      
      val sum = { left: Int, right: Int -> left + right }
    
      fun main(args: Array) {
          println("1 + 3 = ${sum(1, 3)}")
          println("3 + 6 = ${sum(3, 6)}")
          println("83 + 134 = ${sum(83, 134)}")
      }
      
      

    Here it is using string concatenation:

      
      val sum = { left: Int, right: Int -> left + right }
    
      fun main(args: Array) {
          println("1 + 3 = " + sum(1, 3))
          println("3 + 6 = " + sum(3, 6))
          println("83 + 134 = " + sum(83, 134))
      }
      
      

    This solution is also available in the learning path GitHub repo in the Kotlin/Unit_7 folder, called ProgrammingProblem1.kt.

    2. Solution

      
      fun smartCast(number: Number) {
          if (number is Byte) println("The number parameter is of type Byte")
          else if (number is Short) println("The number parameter is of type Short")
          else if (number is Int) println("The number parameter is of type Int")
          else if (number is Long) println("The number parameter is of type Long")
          else if (number is Float) println("The number parameter is of type Float")
          else if (number is Double) println("The number parameter is of type Double")
          else println("The number parameter is of type UNKNOWN")
      }
    
      fun main(args: Array) {
          val number = 23
          smartCast(number.toByte())
          smartCast(number.toShort())
          smartCast(number.toInt())
          smartCast(number.toLong())
          smartCast(number.toFloat())
          smartCast(number.toDouble())
          smartCast("This is a string!")
      }
      
      

    This solution is also available in the learning path GitHub repo in the Kotlin/Unit_7 folder, called ProgrammingProblem2.kt.