Checkpoint: Functions & OOP
Reviewed & published by Brayan K
This checkpoint consolidates the whole unit — higher-order and inline functions, sequences, interfaces, objects, delegation, coroutines, and Flow — into one build challenge and a quiz.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Work through the recap, complete the multi-step build challenge (a solution is provided), and test yourself with the checkpoint quiz before moving on to the capstone.
What This Checkpoint Covers
- Higher-order functions & function types
- Inline functions and reified type parameters
- Sequences (lazy collections)
- Interfaces, abstract classes, and objects
- Delegation and delegated properties
- Coroutine scope, dispatchers, and Flow
1️⃣ One-File Recap of the Whole Unit
Before the challenge, here is everything you've learned in a single runnable program: an interface with a default method, an object declaration singleton, an object expression, a higher-order function, a lazy sequence pipeline, and a delegated property. Read it top to bottom and predict each line of output.
import kotlin.properties.Delegates
// A whirlwind recap of everything in this unit, in one runnable file.
interface Animal { // INTERFACES
val name: String
fun sound(): String
fun describe(): String = "$name says ${sound()}" // default method
}
object Zoo { // OBJECT DECLARATION (singleton)
var visitors = 0
fun admit() { visitors++ }
}
// HIGHER-ORDER FUNCTION: takes a transform function as a parameter
fun transformAll(values: List<Int>, op: (Int) -> Int): List<Int> =
values.map(op)
fun main() {
val dog = object : Animal { // OBJECT EXPRESSION implementing interface
override val name = "Dog"
override fun sound() = "Woof"
}
println(dog.describe()) // -> Dog says Woof
Zoo.admit(); Zoo.admit()
println("visitors = ${Zoo.visitors}") // -> visitors = 2
// HIGHER-ORDER + SEQUENCE: lazy pipeline
val result = (1..6).asSequence()
.filter { it % 2 == 0 } // 2, 4, 6
.map { it * it } // 4, 16, 36
.toList()
println(result) // -> [4, 16, 36]
println(transformAll(listOf(1, 2, 3)) { it + 10 }) // -> [11, 12, 13]
var score: Int by Delegates.observable(0) { _, old, new -> // DELEGATION
println("score $old -> $new")
}
score = 5 // -> score 0 -> 5
}If every line made sense, you're ready for the build challenge. If any surprised you, revisit that lesson before continuing.
2️⃣ Build Challenge: A Reporting Pipeline
Now put it together yourself. The challenge below asks you to combine an interface (with a default method), an object singleton that implements it, and a higher-order function running a lazy sequence pipeline. Try it first; the full, hand-verified solution is right below in a collapsible panel.
// 🎯 BUILD CHALLENGE — combine the whole unit.
// Save as Report.kt and run main().
//
// You will build a tiny reporting pipeline that uses:
// • an INTERFACE with a default method
// • an OBJECT DECLARATION (singleton) implementing that interface
// • a HIGHER-ORDER FUNCTION running over a SEQUENCE (lazy)
//
// STEP 1: Define an interface Report with:
// fun format(n: Int): String
// a DEFAULT fun banner(): String returning "=== REPORT ==="
//
// STEP 2: Make an object declaration PlainReport implementing Report,
// where format(n) returns "value=" + n.
//
// STEP 3: Write a higher-order function
// fun process(nums: List<Int>, keep: (Int) -> Boolean): List<Int>
// that returns, as a LAZY sequence pipeline materialized to a list,
// the numbers passing 'keep', each doubled.
//
// STEP 4: In main():
// - print PlainReport.banner()
// - call process(listOf(1,2,3,4,5)) keeping EVEN numbers
// - print PlainReport.format(...) for EACH result
//
// ✅ Expected output:
// === REPORT ===
// value=4
// value=8
fun main() {
// your code here — see the solution below if you get stuck
}Here is one complete, correct implementation. The output is shown in the comment at the bottom.
Pitfalls Across the Unit
- ❌ Sequence with no terminal op — the pipeline never runs. ✅ End with toList(), sum(), etc.
- ❌ Forgetting override on an interface member. ✅ Every implemented member needs it.
- ❌ Constructing an object declaration with (). ✅ Use the name directly: PlainReport.banner().
- ❌ reified without inline. ✅ Reified type params require an inline function.
- ❌ Collecting a Flow outside a coroutine. ✅ collect is suspending — call it in runBlocking.
📝 Checkpoint Quiz
Answer each in your head, then expand to check. No peeking first!
It describes a function taking two Ints and returning an Int. If add holds such a value, you invoke it with add(2, 3). A lambda { a, b -> a + b } or a reference ::plus can supply the value.
Generics are erased at runtime, so is T normally can't compile. Inlining copies the function body into each call site, where the concrete type is known and can be substituted — that substitution is exactly what reified relies on, so it's only allowed on inline functions.
Nothing — and the map never even runs. A sequence is lazy; without a terminal operation like toList() or forEach, no values are pulled through the pipeline.
An object declaration (object Name { }) is a single named singleton shared across the program. An object expression (object : Type { }) is an anonymous one-off object created fresh each time the line runs — Kotlin's anonymous class.
It defers computing the value until the first read, then caches the result so later reads reuse it without recomputing. A normal val is computed when the object is constructed.
launch is fire-and-forget and returns a Job; async returns a Deferred<T> whose result you await(). A Flow is a cold, lazy stream of many values over time that runs only when collected and can suspend at each step.
Study Tips Before the Capstone
- 💡 Re-derive the recap output by hand — if you can predict every line, the unit has stuck.
- 💡 Rebuild the challenge from scratch without peeking, then diff against the solution.
- 💡 List which tool fits which job: sequence for lazy pipelines, object for singletons, delegation to avoid boilerplate, coroutines for concurrency.
🎉 Checkpoint Complete!
- ✅ You recapped higher-order & inline functions and sequences
- ✅ You combined an interface, a singleton object, and a sequence pipeline
- ✅ You reviewed delegation, coroutines, and Flow
- ✅ You self-tested with the checkpoint quiz
- ✅ You're ready to apply it all in a real project
Practice quiz
What does the function type (Int) -> Int describe?
- A function taking an Int and returning an Int
- An Int variable named Int
- A range from Int to Int
- A class with one Int field
Answer: A function taking an Int and returning an Int. (Int) -> Int is a function type: it takes one Int parameter and returns an Int.
Why must a function be marked inline to use a reified type parameter?
- Reified is just a style choice
- Inlining copies the body into each call site where the concrete type is known
- Reified makes the function faster
- Inline functions cannot have generics
Answer: Inlining copies the body into each call site where the concrete type is known. Generics are erased at runtime, but inlining substitutes the concrete type at each call site, which is what reified relies on.
What happens to listOf(1,2,3).asSequence().map { it * 2 } with no terminal operation?
- It prints [2, 4, 6]
- It throws an exception
- Nothing runs at all
- It returns a List of doubled values
Answer: Nothing runs at all. Sequences are lazy; without a terminal operation like toList(), no element is ever pulled through the pipeline.
What is the difference between an object declaration and an object expression?
- They are identical
- A declaration is anonymous; an expression is named
- A declaration is a named singleton; an expression is an anonymous one-off object
- Only object expressions can implement interfaces
Answer: A declaration is a named singleton; an expression is an anonymous one-off object. object Name {} is a single named singleton, while object : Type {} creates a fresh anonymous object each time the line runs.
What does val x by lazy { ... } provide over a normal val?
- It recomputes the value on every read
- It defers computation until first read, then caches the result
- It makes the value mutable
- It runs the value on a background thread
Answer: It defers computation until first read, then caches the result. lazy delays the computation until the first access and caches the result for later reads.
In coroutines, what does launch return?
- A Deferred<T>
- A Job
- The computed result directly
- A Flow
Answer: A Job. launch is fire-and-forget and returns a Job; async returns a Deferred<T> whose result you await().
What is a Flow in Kotlin?
- A single suspending value
- A cold, lazy stream of values that runs only when collected
- A thread pool
- A synchronous list
Answer: A cold, lazy stream of values that runs only when collected. A Flow is a cold, lazy asynchronous stream that emits values only when collected.
Which keyword forwards an interface implementation to a wrapped object without boilerplate?
- by
- with
- delegate
- forward
Answer: by. Class delegation uses by, as in class C(impl: I) : I by impl, forwarding all members to impl.
Why does a higher-order function need a function type parameter like op: (Int) -> Int?
- To accept another function as an argument
- To return an Int constant
- To declare a generic class
- To mark the function inline
Answer: To accept another function as an argument. A higher-order function takes or returns functions; the function-type parameter lets you pass behavior in, e.g. values.map(op).
When does a sequence pipeline actually execute its filter and map steps?
- Immediately when defined
- When a terminal operation like toList() or sum() is called
- Only inside a coroutine
- When the program exits
Answer: When a terminal operation like toList() or sum() is called. Sequence intermediate operations are lazy; the chain runs only when a terminal operation pulls the values through.
Continue this course
- Previous: Flow & Channels
- Next: Annotations & Reflection — Declare and read annotations, and inspect classes at runtime with ::class
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What should I be able to do after this checkpoint?
You should be comfortable declaring and passing function types, using higher-order functions and trailing lambdas, marking functions inline and using reified type parameters, building lazy pipelines with sequences, designing interfaces and abstract classes, creating singleton object declarations and anonymous object expressions, applying class and property delegation with by, and running concurrent work with coroutine scopes, dispatchers, and Flow. The build challenge weaves several of these together so you can confirm they click as a whole rather than in isolation.
I'm shaky on sequences versus regular collection operations — quick reminder?
A list operation chain is eager: each step runs over the whole list and produces a new intermediate list before the next step. A sequence is lazy: calling asSequence() makes each element flow through the entire chain one at a time, and no work happens until a terminal operation like toList(), sum(), or first() pulls the values. Sequences shine for long chains, large data, and early exits because they avoid intermediate collections and can stop as soon as they have what they need.
When do I reach for an interface, an object, or delegation?
Use an interface to declare capabilities a type must provide, optionally with default methods, and implement many of them on one class. Use an object declaration when you need exactly one shared instance — a singleton service, logger, or config holder — and a companion object for class-bound factories and constants. Use delegation with by to forward an interface to a wrapped implementation without boilerplate, or to hand a property's get/set to a helper like lazy, observable, or a custom getValue/setValue delegate.
How do coroutines and Flow fit with the rest?
Coroutines give you structured concurrency: a CoroutineScope tracks the coroutines you launch, dispatchers choose the threads, launch returns a Job while async returns a Deferred you await, and withContext switches dispatcher and returns a result. Flow builds on the same machinery to model asynchronous streams — cold, lazy sequences of values that can suspend at each step and run only when collected, with a Channel available for true coroutine-to-coroutine handoff. Together they cover both 'run work concurrently' and 'stream values over time'.