How do takeIf and takeUnless differ from let, run, and a filter on a single value? When would you reach for each?
answer
- takeIf/takeUnless = selector (keep or null)
- let/run = transformer (map to R)
- filter/filterNot = collection version
- also/apply = side-effect, return receiver
- Compose: takeIf {}?.let {} ?: default
basics
~10 stakeIf/takeUnless decide whether to keep a value (returning it or null). let/run transform a value into something else. filter works on collections, not single values. Use takeIf to conditionally keep, let to map.
solid answer
~40 s`takeIf`/`takeUnless` are *selectors*: the lambda is a `(T) -> Boolean` predicate and the function returns the **unchanged receiver or null**. `let` and `run` are *transformers*: their lambda returns an arbitrary type `R`, so they map the receiver to a new value (`let` exposes it as `it`, `run` as `this`). `filter` is a collection operation — it removes elements from an `Iterable`/`Sequence`; it has no single-value form, so for one value you use `takeIf`. A common composition is `value.takeIf { predicate }?.let { transform(it) }`: gate first, then map only when kept. Reach for `takeIf`/`takeUnless` when the question is "keep this value or not"; reach for `let`/`run` when the question is "turn this value into something else".
code
kotlin · 9 lines// Selector then transformer
val normalized = email
.takeIf { it.contains('@') } // String? keep only if looks like email
?.let { it.trim().lowercase() } // transform only survivors
?: throw IllegalArgumentException("bad email")
// Collection analogue
val positives = numbers.filter { it > 0 } // like takeIf for each element
val nonZero = numbers.filterNot { it == 0 } // like takeUnlessgo deeper
Knows takeIf keeps-or-drops while let maps, even if fuzzy on the scope-function family.
Clearly separates selector (takeIf/takeUnless) from transformer (let/run) and side-effect (also/apply), and names filter/filterNot as the collection analogues.
Composes takeIf{}?.let{} ?: default fluently and explains why let cannot substitute for takeIf as a gate.
Establishes idiom conventions across the team so scope functions are chosen by intent, keeping chains readable and reviewable.
## The three roles ### Selector: takeIf / takeUnless ```kotlin inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? ``` The lambda is a **predicate** returning `Boolean`. The function returns the **same receiver** (or `null`). It never changes the value's type or contents — it only decides keep-or-drop. ### Transformer: let / run ```kotlin inline fun <T, R> T.let(block: (T) -> R): R inline fun <T, R> T.run(block: T.() -> R): R ``` The lambda returns **`R`**, an arbitrary type. `let` passes the receiver as the parameter `it`; `run` makes it the lambda receiver `this`. These **map** one value to another. (`also`/`apply` are the side-effecting siblings that return the receiver itself.) ### Collection filter ```kotlin fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> ``` `filter` operates on a collection and returns a new collection of the matching elements. There is **no single-value `filter`** — `takeIf` is effectively "filter for one value", yielding `T?` instead of a list. ## Picking one - "Keep this value only if a condition holds" -> `takeIf` / `takeUnless`. - "Convert this value into another value/type" -> `let` / `run`. - "Run a side effect and keep the value" -> `also` / `apply`. - "Filter many elements" -> `filter` (or `filterNot`, the collection analogue of `takeUnless`). ## Powerful composition Gate then transform, all null-safe: ```kotlin val slug = title .takeIf { it.isNotBlank() } // T? — keep only non-blank ?.let { it.lowercase().replace(' ', '-') } // transform survivors ?: "untitled" // fallback when dropped ``` Here `takeIf` filters the single value, `?.let` transforms only if it survived, and `?:` supplies a default. Mixing them up — e.g. trying to filter a value with `let`, which always runs and never returns null on its own — is a common mistake. ## Subtle point `let` always executes its block and returns its result; it does not introduce nullability by itself. `takeIf` *introduces* a `null` possibility deliberately. So they are not interchangeable: replacing `takeIf { p }` with `let { if (p) it else null }` works but is more verbose and the very thing `takeIf` was made to shorten.
- What is the collection-level equivalent of takeUnless?filterNot — it keeps the elements for which the predicate is false.
- Why can't you 'filter' a single value with let?let always runs and returns its block's result; it doesn't drop the value to null on its own, so it can't act as a gate the way takeIf does.
saying these in an interview costs you the question
- Saying takeIf transforms the value into a new type
- Saying let returns null when its predicate is false (let has no predicate)
- Using filter on a single non-collection value
- Not knowing also/apply return the receiver while let/run return the block result