Lesson 6 gave the planner a clear owner and stable task identifiers. We can now request a snapshot without handing another function control of its storage. The next requirement is familiar in small applications: different views of the same data. One report lists unfinished tasks, another lists short sessions, and another summarizes completed work. Copying a separate reporting loop for each view would work initially, but the copies would become awkward to keep consistent.
This lesson introduces a higher-order function for a reason you can see. We pass a selection rule into a reporting function while keeping sorting, totals, and display formatting in one place. The practical goal is not to show off concise syntax. It is to make the shared behavior easy to maintain and the differences easy to read. Every report is a value you can inspect before anything is printed.
Write the report contract first
A report has a heading, a list of display lines, and a total measured in minutes. Each line identifies a task and includes its duration. Selected tasks appear from shortest to longest duration. Tasks with equal durations are ordered by identifier, so two otherwise similar inputs still have an explicit ordering rule. When no task matches, the report contains no lines and its total is zero. Rendering then supplies a useful empty message.
Those decisions are small but significant. Without an agreed empty case, someone might print nothing and mistake it for a broken report. Without a defined order, a screenshot from one test might look different from another even when the selected tasks are correct. A report contract tells you which differences matter and which implementation details should stay out of the reader's way.
Recognize the part that changes
Across our three reports, only the heading and selection rule vary. Pending work selects tasks whose done flag is false. Short pending sessions add a duration threshold to that condition. Completed work selects tasks whose flag is true. Each rule receives a StudyTask and returns a Boolean. The reporting function therefore accepts a parameter with the function type (StudyTask) -> Boolean.
The name include describes what that parameter means to the caller. Inside the function, filter uses it to decide which tasks belong in the selected collection. We could also pass a named function with a function reference, but lambdas keep these short rules beside their report headings. If a condition later grows into several business decisions, extract it and name it rather than burying a miniature application inside braces.
Run a complete reporting program
Use this example independently in Playground. It repeats the current task model so you do not need to assemble files from earlier lessons. In your own planner project, keep one model declaration and call these reporting functions with the snapshot returned by StudyPlanner.
data class StudyTask(
val id: Int,
val title: String,
val minutes: Int,
val done: Boolean = false
)
data class StudyReport(
val heading: String,
val lines: List<String>,
val totalMinutes: Long
)
fun buildReport(
tasks: List<StudyTask>,
heading: String,
include: (StudyTask) -> Boolean
): StudyReport {
val selected = tasks.filter(include).sortedWith(
compareBy<StudyTask> { it.minutes }.thenBy { it.id }
)
val lines = selected.map { task ->
"#${task.id} ${task.title}: ${task.minutes} min"
}
val total = selected.sumOf { it.minutes.toLong() }
return StudyReport(heading, lines, total)
}
fun renderReport(report: StudyReport): String {
val content = if (report.lines.isEmpty()) {
"No matching tasks"
} else {
report.lines.joinToString("\n")
}
return "${report.heading}\n$content\nTotal: ${report.totalMinutes} min"
}
fun main() {
val tasks = listOf(
StudyTask(1, "Review variables", 15, true),
StudyTask(2, "Practice functions", 20),
StudyTask(3, "Build planner", 25)
)
val before = tasks.toList()
val pending = buildReport(tasks, "Pending study") { !it.done }
val short = buildReport(tasks, "Short pending sessions") {
!it.done && it.minutes <= 20
}
val completed = buildReport(tasks, "Completed study") { it.done }
check(pending.totalMinutes == 45L)
check(short.totalMinutes == 20L)
check(completed.totalMinutes == 15L)
check(pending.lines == listOf(
"#2 Practice functions: 20 min",
"#3 Build planner: 25 min"
))
val empty = buildReport(tasks, "Very short sessions") { it.minutes < 5 }
check(empty.lines.isEmpty())
check(empty.totalMinutes == 0L)
check(renderReport(empty).contains("No matching tasks"))
val tied = buildReport(listOf(
StudyTask(8, "Second", 10),
StudyTask(4, "First", 10)
), "Equal durations") { true }
check(tied.lines.first() == "#4 First: 10 min")
check(tasks == before)
println(renderReport(pending))
println()
println(renderReport(short))
println()
println(renderReport(completed))
println("Reporting checkpoint passed")
}
The pending report has two task lines and a total of 45 minutes. The short report contains only Practice functions and totals 20. Completed study contains Review variables and totals 15. The example also checks an empty selection, an equal-duration ordering case, and preservation of the original task values. Those checks focus on promises a reader depends on, rather than merely proving that a particular chain of collection calls exists.
Keep calculation separate from output
buildReport returns structured data; renderReport returns text; main prints it. If a total is wrong, inspect the selected tasks and the calculation without looking through console formatting. If a heading or separator is wrong, inspect rendering without changing task selection. This separation is useful even in a tiny program because it makes experiments cheap. You can compare a report value to an expected value before deciding how it should appear.
Mixing println into the selection lambda is especially tempting while debugging. It makes the rule do two things and creates output every time selection runs. Use temporary prints if they help investigate a failure, then remove them from the final rule. In this lesson the lambda should answer one question about one task. It should neither complete tasks nor update an unrelated counter in the surrounding program.
Follow the collection stages in order
The selected variable makes the reporting pipeline visible. First we filter according to the requested rule. Then we sort the matching tasks. Next we map those task values into display lines. Finally we sum their durations. The code uses ordinary list operations, which are appropriate for this small planner. There is no reason to introduce lazy sequences simply because a pipeline contains several steps.
Each stage has a useful debugging question. If the report includes a completed task, inspect selection. If the right tasks appear in the wrong order, inspect sorting. If identifiers or durations disappear, inspect line formatting. If the total disagrees with the displayed entries, confirm that both were built from the same selected collection. This is a calmer process than rewriting the entire expression whenever one output seems surprising.
Choose types that preserve meaning
Each session duration remains an Int because the model accepts a small bounded number of minutes. The report total is a Long because it represents an aggregate over potentially many tasks. Conversion happens before summing. This choice expands the available range but does not promise unlimited arithmetic. A program accepting unbounded collections would still need an explicit limit or overflow policy, along with checks that match its actual workload.
StudyReport keeps both the lines and the total so the renderer does not have to parse numbers back out of text. That is an important maintenance habit: preserve useful structure until the presentation boundary. A string can be excellent output and a poor internal representation. Once calculations depend on splitting your own display sentence, a harmless wording edit can unexpectedly change program behavior.
Use readable lambda names as expressions grow
The short selection rule uses it because there is one small input and its meaning is obvious beside the heading. The mapping lambda uses task because interpolation reads more naturally with a descriptive name. Neither form is a universal style rule. The important question is whether someone reviewing a condition can identify the value being examined without retracing nested scopes.
When you add another threshold, give it a name outside the lambda. A variable such as shortSessionLimit makes the policy visible and keeps a magic number from spreading across reports. If the function accepts that limit from user input later, validate it at the boundary before building the selection rule. Reusable behavior still needs dependable inputs; passing a function does not remove the need for product decisions.
Practice a report you can verify by hand
Add a fourth task lasting ten minutes and leave it unfinished. Predict the new pending total of 55 and short pending total of 30 before running the program. The new ten-minute line should appear before the twenty-minute line. Then complete that task by creating a copy in a new list and rebuild the reports. Check that the old reports remain unchanged because they describe the earlier input snapshot.
Create another view called Long pending sessions that selects unfinished tasks lasting at least 25 minutes. Write its expected lines and total explicitly, including the empty case. If you cannot calculate the expected answer by hand for three or four tasks, reduce your sample before debugging code. Small, understandable fixtures make a wrong condition visible much faster than a large invented dataset.
Bring reporting into the final project
You are ready for lesson 8 when you can change a selection rule without touching sorting or formatting, explain the function parameter type, and demonstrate that building a report leaves its input unchanged. Save the reporting functions alongside the model and planner from earlier lessons. The final integration will connect text input, state ownership, and reporting in one repeatable workflow with checks at the boundaries between them.
For precise language details, review higher-order functions and lambdas, collection transformations, and collection ordering. Treat the example as a small design exercise: identify what varies, name the stable behavior, and verify both separately. That habit matters more than remembering the shortest possible spelling of a lambda.
