LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 2: Have you double-checked your code?

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Have you double-checked your code?

The function below displays a progress bar, accepting progress values ranging from 0 to 1.

/**
 * Shows a progress bar blocking other UI interaction with a given progress
 * ratio in \[0, 1\].
 */
fun showProgressBar(progress: Float)

I implemented the progress bar to only accept values between 0 and 1 as shown below. In this case, caller refers to the code using showProgressBar. Also, coerceAtMost and coerceAtLeast are functions used to constrain a number to a maximum or minimum limit, respectively (they serve the same purpose as min and max).

fun caller() {
    ... // snip
    val cappedProgress = progress.coerceAtMost(1F)
    showProgressBar(cappedProgress)
}

fun showProgressBar(progress: Float) {
    val progressInRange = progress.coerceAtLeast(0F)
    ... // snip
}

What's the issue with this code?

It's fine as long as someone else checks the code

The issue with this code is the ambiguity regarding who is responsible for ensuring the values are within the [0, 1] range. The upper limit check with coerceAtMost is handled by the caller, while the lower limit check with coerceAtLeast is done by the callee. This not only makes the function prone to misuse but also increases the risk of bugs during specification or implementation changes.

As a fundamental principle, function (or method) calls should be foolproof. Here are two ways to achieve this.

Option 1: Rely only on yourself

The first option involves performing checks within the callee. This is particularly effective in scenarios where you can't safely manage checked and unchecked states.

Since a regular Float isn't limited to the [0, 1] range, it's challenging to guarantee that a Float argument will always fall within this range. The argument might be a user-entered value or one provided by an external system. In most cases, it's safer to consider the argument as unreliable.

In the implementation below, if a number outside the [0, 1] range is provided, it's treated as 0 or 1, accordingly.

fun showProgressBar(progress: Float) {
    val progressInRange = progress.coerceIn(0F, 1F)
    ... // snip
}

If the caller wishes to handle out-of-range values differently, one approach is to inform them of the operation's success via a return value. In the code below, if an out-of-range argument is provided, it returns false.

fun showProgressBar(progress: Float): Boolean {
    if (progress !in 0F..1F) {
        return false
    }

    ... // snip
    return true
}

To make the caller aware of potential errors, you could enforce return value handling (like Java's Error Prone library's @CheckReturnValue, Android Kotlin's @CheckResult, or typical functions in Swift or Rust) or use checked exceptions.

Option 2: Type-safe "checked certificates"

The second method is to create a type that ensures a value falls within a specific range, thereby guaranteeing that showProgressBar only receives correct values.

For the showProgressBar example, creating a "checked" model like the one below would be ideal.

class ProgressRatio(rawValue: Float) {
    val value = rawValue.coerceIn(0F..1F)
}

fun showProgressBar(progressRatio: ProgressRatio)

If the caller wants to handle out-of-range values, creating a factory function (or a failable initializer) often works well.

class ProgressRatio private constructor(val value: Float) {
    companion object {
        fun of(value: Float): ProgressRatio? =
            if (value in 0F..1F) ProgressRatio(value) else null
    }
}

Using a type-safe value (like null or Optional) to indicate an error in the factory function makes the code more robust. Using unchecked exceptions (like IllegalArgumentException) can make the class difficult to use without detailed knowledge.


In summary: Avoid code that implicitly assumes "it has been checked."

Keywords: implicit dependency, state check logic, type safety