Item 17: Consider naming arguments

When you read a code, it is not always clear what an argument means. Take a look at the following example:

  1. val text = (1..10).joinToString("|")

What is "|"? If you know joinToString well, you know that it is the separator. Although it could just as well be the prefix. It is not clear at all3. We can make it easier to read by clarifying those arguments whose values do not clearly indicate what they mean. The best way to do that is by using named arguments:

  1. val text = (1..10).joinToString(separator = "|")

We could achieve a similar result by naming variable:

  1. val separator = "|"
  2. val text = (1..10).joinToString(separator)

Although naming the argument is more reliable. A variable name specifies developer intention, but not necessarily correctness. What if a developer made a mistake and placed the variable in the wrong position? What if the order of parameters changed? Named arguments protect us from such situations while named values do not. This is why it is still reasonable to use named arguments when we have values named anyway:

  1. val separator = "|"
  2. val text = (1..10).joinToString(separator = separator)

When should we use named arguments?

Clearly, named arguments are longer, but they have two important advantages:

  • Name that indicates what value is expected.
  • They are safer because they are independent of order.

The argument name is important information not only for a developer using this function but also for the one reading how it was used. Take a look at this call:

  1. sleep(100)

How much will it sleep? 100 ms? Maybe 100 seconds? We can clarify it using a named argument:

  1. sleep(timeMillis = 100)

This is not the only option for clarification in this case. In statically typed languages like Kotlin, the first mechanism that protects us when we pass arguments is the parameter type. We could use it here to express information about time unit:

  1. sleep(Millis(100))

Or we could use an extension property to create a DSL-like syntax:

  1. sleep(100.ms)

Types are a good way to pass such information. If you are concerned about efficiency, use inline classes as described in Item 46: Use inline modifier for functions with parameters of functional types. They help us with parameter safety, but they do not solve all problems. Some arguments might still be unclear. Some arguments might still be placed on wrong positions. This is why I still suggest considering named arguments, especially for parameters:

  • with default arguments,
  • with the same type as other parameters,
  • of functional type, if they’re not the last parameter.

Parameters with default arguments

When a property has a default argument, we should nearly always use it by name. Such optional parameters are changed more often than those that are required. We don’t want to miss such a change. Function name generally indicates what are its non-optional arguments, but not what are its optional ones. This is why it is safer and generally cleaner to name optional arguments.4

Many parameters with the same type

As we said, when parameters have different types, we are generally safe from placing an argument at an incorrect position. There is no such freedom when some parameters have the same type.

  1. fun sendEmail(to: String, message: String) { /*...*/ }

With a function like this, it is good to clarify arguments using names:

  1. sendEmail(
  2. to = "contact@kt.academy",
  3. message = "Hello, ..."
  4. )

Parameters of function type

Finally, we should treat parameters with function types specially. There is one special position for such parameters in Kotlin: the last position. Sometimes a function name describes an argument of a function type. For instance, when we see repeat, we expect that a lambda after that is the block of code that should be repeated. When you see thread, it is intuitive that the block after that is the body of this new thread. Such names only describe the function used at the last position.

  1. thread {
  2. // ...
  3. }

All other arguments with function types should be named because it is easy to misinterpret them. For instance, take a look at this simple view DSL:

  1. val view = linearLayout {
  2. text("Click below")
  3. button({ /* 1 */ }, { /* 2 */ })
  4. }

Which function is a part of this builder and which one is an on-click listener? We should clarify it by naming the listener and moving builder outside of arguments:

  1. val view = linearLayout {
  2. text("Click below")
  3. button(onClick = { /* 1 */ }) {
  4. /* 2 */
  5. }
  6. }

Multiple optional arguments of a function type can be especially confusing:

  1. fun call(before: ()->Unit = {}, after: ()->Unit = {}){
  2. before()
  3. print("Middle")
  4. after()
  5. }
  6. call({ print("CALL") }) // CALLMiddle
  7. call { print("CALL") } // MiddleCALL

To prevent such situations, when there is no single argument of a function type with special meaning, name them all:

  1. call(before = { print("CALL") }) // CALLMiddle
  2. call(after = { print("CALL") }) // MiddleCALL

This is especially true for reactive libraries. For instance, in RxJava when we subscribe to an Observable, we can set functions that should be called:

  • on every received item
  • in case of error,
  • after the observable is finished.

In Java I’ve often seen people setting them up using lambda expressions, and specifying in comments which method each lambda expression is:

  1. // Java
  2. observable.getUsers()
  3. .subscribe((List<User> users) -> { // onNext
  4. // ...
  5. }, (Throwable throwable) -> { // onError
  6. // ...
  7. }, () -> { // onCompleted
  8. // ...
  9. });

In Kotlin we can make a step forward and use named arguments instead:

  1. observable.getUsers()
  2. .subscribeBy(
  3. onNext = { users: List<User> ->
  4. // ...
  5. },
  6. onError = { throwable: Throwable ->
  7. // ...
  8. },
  9. onCompleted = {
  10. // ...
  11. })

Notice that I changed function name from subscribe to subscribeBy. It is because RxJava is written in Java and we cannot use named arguments when we call Java functions. It is because Java does not preserve information about function names. To be able to use named arguments we often need to make our Kotlin wrappers for those functions (extension functions that are alternatives to those functions).

Summary

Named arguments are not only useful when we need to skip some default values. They are important information for developers reading our code, and they can improve the safety of our code. We should consider them especially when we have more parameters with the same type (or with functional types) and for optional arguments. When we have multiple parameters with functional types, they should almost always be named. An exception is the last function argument when it has a special meaning, like in DSL.