JavaRush/Courses/Kotlin SELF/takeIf and takeUnless: guard conditions and pairing with ...

takeIf and takeUnless: guard conditions and pairing with Elvis ?:

Open

1. Introduction

When you’re just starting to write code, if feels like a universal screwdriver: you can use it to check, assign, print, and even “educate” the user with a message like “enter it properly”. And that’s true. But over time (sometimes after just two source files) a new problem appears: you often don’t need to “branch”, you need to “filter a value”.

Imagine a typical situation from our console application (we continue to evolve a CLI app with commands like add, list, remove, etc.). The user enters a string, and we want to:

  • take it if it’s reasonable (not empty, not just spaces, not -1, not "abc" instead of a number),
  • otherwise get null so we can nicely handle it further using the already familiar null-safety and Elvis ?:.

This is where takeIf and takeUnless act like small “filters” for a value.

2. What takeIf is: “keep the value if it passes the check”

takeIf is an extension function that is called on the value itself and returns:

  • the same value if the condition is true,
  • null if the condition is false.

So the logic reads roughly like: “take this, if…”.

You can remember the signature not as scary math, but as meaning:

value.takeIf { conditionAboutValue }  // returns value or null

The key effect: the result becomes nullable (T?), which means we can then apply:

  • safe call ?.
  • Elvis ?:
  • a regular if check (x != null)

In Kotlin documentation and examples, you’ll often see the style .takeIf { it >= 0 } ?: ... as a compact way to “take the value only if it’s valid”.

Mini table for your brain

Tool Idea Returns
takeIf { cond }
“Keep the value if cond = true”
T?
takeUnless { cond }
“Keep the value if cond = false”
T?
?:
“If the left side is null, take the right side” non-null result (usually)

3. Elvis ?: in this pairing: “if it didn’t fit—substitute a fallback”

You met the Elvis operator in the null-safety topic: an expression of the form

val x = nullableValue ?: defaultValue

means: if the left side is null, take defaultValue.

Why does takeIf almost always pair well with ?:? Because takeIf intentionally gives you null as a “marker of invalidity”, and ?: turns that marker into clear behavior: a default value, an error message, an early return from a function, or something else.

4. Example: validating the result of indexOf

Now a real-life string situation: indexOf(...) returns -1 if it didn’t find anything. That’s not an exception and not a JVM error; it’s just “the agreed behavior”. But you can’t use -1 as an index for substring.

Usually people write it like this:

val at = email.indexOf('@')
if (at >= 0) { ... } else { ... }

But you can do it like this—via takeIf + Elvis:

fun main() {
    val email = "aliceexample.com"
    val at = email.indexOf('@')

    val safeAt = at.takeIf { it >= 0 } ?: -1
    println(safeAt) // -1
}

Okay, this isn’t super useful yet: we just returned -1. More useful is to use a fallback that is guaranteed safe.

For example, sometimes it’s convenient to say: “if there is no "@", consider that there is no domain”:

fun main() {
    val email = "aliceexample.com"
    val at = email.indexOf('@')

    val domain = at.takeIf { it >= 0 }
        ?.let { email.substring(it + 1) } // (we'll cover let later; today without it)
        ?: "INVALID"

    println(domain) // INVALID
}

But wait. We haven’t covered scope functions on this day (that will be later), so let’s do the same thing without let, honestly and clearly:

fun main() {
    val email = "aliceexample.com"
    val at = email.indexOf('@')

    val domain = if (at.takeIf { it >= 0 } != null) {
        email.substring(at + 1)
    } else {
        "INVALID"
    }

    println(domain) // INVALID
}

Here you can see an important idea: takeIf is not a “replacement for if forever”, it’s a tool that lets you turn a “bad value” into null.

In official Kotlin examples you can find a similar trick: they search for an index, then do .takeIf { it >= 0 } ?: s.length, to get a safe boundary.

5. Example: “an empty string after trim” is also invalid input

The user entered " " (three spaces). Formally, it’s not an empty string (length = 3), but its meaning is like the promise “I’ll start studying on Monday”—that is, zero.

A common pattern: first trim(), then a check.

takeIf lets you write this very linearly:

fun main() {
    val raw = "   "
    val name = raw.trim().takeIf { it.isNotEmpty() } ?: "UNKNOWN"

    println(name) // UNKNOWN
}

Here it reads almost like an English sentence: “take the trimmed string if it’s not empty, otherwise UNKNOWN”.

6. takeUnless: “the same thing, but reversed”

takeUnless is the mirrored sibling of takeIf.

  • takeIf { cond } keeps the value when cond == true
  • takeUnless { cond } keeps the value when cond == false

Why use it if you can write takeIf { !cond }? Because sometimes the inversion reads simpler specifically as “if NOT…”.

Let’s imagine that in our CLI commands we forbid some “reserved words” as a category name, for example "all" (because the command list all can mean “show everything”).

fun main() {
    val rawCategory = "all"

    val category = rawCategory.trim()
        .takeUnless { it.lowercase() == "all" }
        ?: "uncategorized"

    println(category) // uncategorized
}

It reads fine: “take the category if it’s NOT all, otherwise uncategorized”.

7. How this helps in a CLI: neat argument parsing

Now an important practical point. In commands like:

  • add food 120 coffee
  • remove 3
  • list food

user input is always “dirty”: extra spaces, empty arguments, weird numbers, sometimes an empty line altogether.

We already know how to normalize spaces via Regex("""\s+""").replace(...) (that was in the previous lecture). Now we’ll add guard conditions via takeIf/takeUnless, so that each intermediate value either becomes normal or turns into null.

Normalize a token: “string or null”

Let’s make a small helper function. It will come in handy almost everywhere: category name, note text, command.

fun normalizeToken(raw: String): String? {
    val cleaned = raw.trim()
    return cleaned.takeIf { it.isNotEmpty() }
}

And test it:

fun main() {
    println(normalizeToken("  milk  ")) // milk
    println(normalizeToken("   "))      // null
}

Returning null here is not “bad”; it’s a useful signal: next you can do ?: and decide what to do.

Parse a positive amount: “number or null”

In an expense tracker, the amount must be a number, and most often positive. With takeIf it’s convenient to do this without extra branching:

fun parsePositiveIntOrNull(raw: String): Int? {
    val n = raw.trim().toIntOrNull()
    return n?.takeIf { it > 0 }
}

Test it:

fun main() {
    println(parsePositiveIntOrNull(" 120 ")) // 120
    println(parsePositiveIntOrNull("-5"))    // null
    println(parsePositiveIntOrNull("abc"))   // null
}

Two ideas work at once here: toIntOrNull() yields null for non-numeric input, and takeIf { it > 0 } turns “a number, but the wrong one” into null as well. In the end, we have a single “didn’t pass validation” signal.

takeIf + Elvis as a one-line guard

Sometimes you want, in a single expression, to get either a correct value or a fallback.

For example: the user enters remove id. If id is invalid, we use 0 as a marker (or we could print an error, but today we’ll focus on the expression).

fun main() {
    val rawId = " -10 "
    val id = rawId.trim().toIntOrNull()?.takeIf { it >= 1 } ?: 0

    println(id) // 0
}

This is very similar to a classic idiom from the documentation: “take the value if it’s valid, otherwise substitute length/boundary/default”.

8. Mini example: parsing remove with guard expressions

Let’s make a small piece of logic for the remove command. We’re not writing the whole project inside the lecture, but we’ll keep pushing the same style.

Scenario: the user enters a line. We want to get id: Int?, where null means “didn’t work out”.

fun parseRemoveIdOrNull(line: String): Int? {
    val parts = Regex("""\s+""").replace(line.trim(), " ").split(" ")

    if (parts.isEmpty()) return null
    if (parts[0].lowercase() != "remove") return null

    val rawId = parts.getOrNull(1) ?: return null
    return rawId.toIntOrNull()?.takeIf { it >= 1 }
}

Yes, there is an if here. That’s fine. We use takeIf not as a religion, but as a handy “mini filter” at a specific step.

You can test it:

fun main() {
    println(parseRemoveIdOrNull("remove 3"))     // 3
    println(parseRemoveIdOrNull("remove -2"))    // null
    println(parseRemoveIdOrNull("remove abc"))   // null
}

It’s useful to notice: takeIf helps exactly where we “already have a value, but need to check it against a condition”.

9. How to think about takeIf/takeUnless and where they fit

Thinking pattern: “turn bad into null → handle it in a uniform way”

Once you adopt this style, code often starts looking like a validation conveyor. Even without scope functions (we’re not touching them today), you can keep a simple flowchart in mind:

flowchart TD
    A[Raw value] --> B[Normalization: trim/replace]
    B --> C{Condition check}
    C -->|OK| D[Value]
    C -->|FAIL| E[null]
    E --> F[Elvis ?: fallback/message/exit]

Meaning: takeIf/takeUnless are a convenient implementation of the node “condition check → null on failure”.

When takeIf/takeUnless improve readability—and when they get in the way

It’s important not to fall into the typical beginner trap: “oh, a new feature—now I’ll stretch it over the entire project.” That’s how PRs appear after which colleagues start drinking chamomile tea instead of coffee.

takeIf/takeUnless are usually appropriate when the condition is short and directly describes validity. Classic examples: it >= 0, it.isNotEmpty(), it in 1..10. In Kotlin examples you can find a style where a string is turned into a value and immediately filtered by length via takeIf—that’s exactly a good, simple predicate.

When the condition is long, with multiple checks and “and also this, and also that”, a plain if often reads better, because you can break the thought into multiple lines and give variables clear names.

A set of mini examples (5–10 lines)

Sometimes it’s better to see a few short situations once than to hear the definition a hundred times.

Take a string only if it “looks like a command”

fun main() {
    val raw = "   add food 120 coffee   "
    val commandLine = raw.trim().takeIf { it.contains(' ') } ?: ""

    println(commandLine) // add food 120 coffee
}

Drop a token that’s too short

fun main() {
    val token = "ok"
    val good = token.takeIf { it.length >= 3 } ?: "too_short"

    println(good) // too_short
}

takeUnless as “forbid a value”

fun main() {
    val category = "root"
    val safe = category.takeUnless { it == "root" } ?: "guest"

    println(safe) // guest
}

10. Common mistakes when using takeIf/takeUnless

Mistake #1: forgetting the result is nullable and using it like a normal value.
takeIf and takeUnless almost always return T?, even if the original value was T. A beginner writes val x = s.takeIf { ... } and then immediately does x.length. The compiler rightfully complains, and the student doesn’t. The fix is simple: either add Elvis ?:, or check for null, or keep chaining via safe call ?..

Mistake #2: trying to replace an entire “business-logic branch” with takeIf.
If on invalid input you need to print a message, ask for re-entry, log an error, or do different actions—that’s no longer “filtering a value”, it’s branching behavior. In such places, if/when is usually more honest and readable: you’re not hiding important logic in a one-liner “for prettiness”.

Mistake #3: overusing takeUnless when takeIf is easier for the reader.
takeUnless { it == "all" } can look fine, but takeUnless { it.length < 3 } already forces the brain into a double negation. If “take it if …” sounds more natural, use takeIf. If “take it if NOT …” is more natural, use takeUnless.

Mistake #4: writing overly complex conditions directly inside takeIf { ... }.
When the predicate grows into “three conditions, two &&, one ||, plus parentheses”, the code stops being teachable. In that case, it’s better to extract the check into a separate function with a descriptive name (for example, isValidCategoryName(s)), or at least introduce intermediate vals, and only then apply takeIf.

Mistake #5: confusing an “invalidity marker” with the “real meaning of null”.
In our topic, null is a convenient technical signal “didn’t pass the check”. But later in the course you’ll design types so that null doesn’t mean “everything and anything”. So try to ensure that null in your code is either an explicitly expected result (parse...OrNull) or a deliberate “flag” from takeIf/takeUnless, which is immediately handled via ?: or a check.

1
Task
Kotlin SELF,  27 level,  4 lecture
Locked
Guest greeting
Guest greeting
1
Task
Kotlin SELF,  27 level,  4 lecture
Locked
Grade validation
Grade validation
1
Task
Kotlin SELF,  27 level,  4 lecture
Locked
Domain from the address
Domain from the address
1
Task
Kotlin SELF,  27 level,  4 lecture
Locked
Category without ban
Category without ban
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet