Kotlin / Reusable behavior and typed boundaries / Lesson 7
Control evaluation with functions and sequences
Pass behavior as data and distinguish eager collection transformations from lazy sequence evaluation.
On this page
Lesson goals
- Read function types and predict captured-state behavior.
- Explain terminal operations, short-circuiting, and sequence side-effect risks.
A function value has a parameter and result contract
A type such as (Int) -> Boolean describes a function that accepts an Int and produces a Boolean. A higher-order function accepts or returns another function, allowing a calculation to vary its policy without copying the surrounding traversal. A lambda supplies a function value with a body; a callable reference such as ::isPositive refers to a named function. Choose the form that communicates the policy clearly. Passing behavior does not make it pure: the function may read external state, mutate an object, throw, or perform I/O unless its contract restricts those effects.
Inject one policy into a fixed traversal
The sample countMatching function uses count with a supplied predicate. Two callers ask different questions of the same data without changing the traversal. This is a local extension point, not a dependency-injection framework. Function parameters are especially useful when the variation is one small operation; a larger interface may be clearer when several related operations and lifecycle rules must travel together. Keep parameter names and return types explicit at public boundaries so callers know whether a predicate is allowed to change the elements it inspects.
fun countMatching(values: List<Int>, predicate: (Int) -> Boolean): Int =
values.count(predicate)
fun main() {
val values = listOf(-2, 0, 3, 4)
println(countMatching(values) { it > 0 })
println(countMatching(values) { it % 2 == 0 })
}
2
3
Expected output is 2 and 3. Positive values are 3 and 4; even values are -2, 0, and 4. The complete program is illustrative and uncompiled in this environment.
Captured state is read according to execution time
A lambda can capture a variable from its enclosing scope. If that variable is mutable, later invocation can observe a changed value rather than the value at lambda creation. This matters when storing callbacks, constructing lazy pipelines, or running concurrent work. Capture a deliberate immutable snapshot when that is the intended policy. Avoid sharing a mutable counter merely because capture makes it convenient; the compiler does not make compound updates atomic. A small function value still needs clear ownership, lifetime, and thread assumptions when it escapes its immediate caller.
A sequence defers intermediate work until consumption
An eager list pipeline usually completes one transformation across its input before the next step. A sequence can process elements through several intermediate operations as a terminal operation requests them. take can therefore avoid evaluating later elements. The example counts predicate evaluations rather than claiming a speed improvement. Lazy evaluation can change when exceptions and side effects occur, and repeated consumption can repeat work when the sequence permits it. Do not put resource-dependent processing behind a returned sequence if its file or other resource will already be closed before consumption.
fun main() {
var inspected = 0
val pipeline = listOf(1, 2, 3, 4).asSequence()
.filter { inspected += 1; it % 2 == 0 }
.take(1)
println(inspected)
println(pipeline.toList().joinToString())
println(inspected)
}
0
2
2
Expected output is 0, 2, 2. Construction performs no predicate calls; consuming the first even value inspects 1 then 2 and stops. The counter is teaching evidence, not a recommendation for side-effecting pipelines.
Choose laziness for semantics and measured work
Sequences can reduce intermediate allocation or unnecessary traversal for some workloads, but they introduce iteration overhead and are not automatically faster for small collections. Global operations such as sorting still need substantial buffering. Infinite sequences require a bounded terminal path, and a misplaced toList can attempt unbounded consumption. Start with a clear eager implementation, identify an actual allocation or traversal concern, and compare a representative isolated workload when tooling is available. This course claims only reasoned evaluation order and no compiler execution or performance benchmark.
Common errors and fixes
A pipeline does nothing before its terminal operation.
- Why it happens
- Sequence intermediates are lazy.
- How to fix it
- Consume it deliberately or use eager operations when immediate work is intended.
- Prevent it next time
- Keep side effects out of deferred transformations.
A callback observes a changed threshold.
- Why it happens
- The lambda captured a mutable variable rather than a snapshot.
- How to fix it
- Capture an immutable local value if creation-time meaning is required.
- Prevent it next time
- Document callback lifetime and captured-state ownership.
toList never finishes on an infinite sequence.
- Why it happens
- Consumption had no bound.
- How to fix it
- Apply a justified take or another terminating condition.
- Prevent it next time
- Reason about terminal operations before introducing an unbounded producer.
Check your understanding
1. Why does the sequence example print zero first?
Show worked solution
asSequence, filter, and take construct deferred processing. None requests an element until toList runs. The terminal operation then asks for the first matching item, so only 1 and 2 are inspected. An eager filter would inspect the whole supplied list before take receives its result.
2. When might an interface be clearer than three unrelated lambda parameters?
Show worked solution
When the operations share one responsibility, state, lifecycle, or error policy. A named interface can document those relationships and prevent callers from accidentally mixing incompatible functions. Use lambdas for a narrow behavior seam and a cohesive object when the protocol is broader. Neither form removes the need to specify effects and ownership.
Key takeaways
- Function values carry behavior and potentially captured state.
- Laziness changes evaluation time, order, and lifetime obligations.