Learning Scala: Thinking in Expressions Instead of Statements
Developers coming from non-fp languages tend to learn functional programming one feature at a time: lambdas, immutable values, pattern matching, collections APIs. The syntax feels new, maybe elegant, but the “Aha,” the conceptual pivot, arrives much later, if at all. That pivot is learning to think in expressions rather than statements. It’s the bridge between the world we know and the world into which FP invites us.
Imperative programming trains us to describe how to do something: step by step, mutating variables along the way. Functional programming asks: what is the value we are computing? In other words, the functional mindset is expression-oriented. An expression evaluates to a value; statements perform actions. Once you begin to write in expressions, whole categories of complexity simply fall away because there is no longer a timeline of state changes to track, no mutable accumulator holding intermediate truth, and no branch that must “fix up” a variable to satisfy some later condition. Everything becomes a value, and values compose (is that still a buzz word?).
For developers who have lived most of their careers in imperative systems—particularly large Java ecosystems—this shift can feel unfamiliar at first, but then surprisingly liberating.
Catch up on earlier posts to follow along with the Functional Programming Isn’t Just for Academics series:
- Post 1: Why Functional Programming Matters for the Systems We Build Today Post 2: Immutability by Default and the Foundation of Reliable Systems Post 3: Pure Functions: Your First Step Toward Bug-Free Concurrency
Each installment dives deeper, illustrating how Scala enables teams to write clean, testable, and scalable applications.
The Imperative Habit: Code as a Sequence of Events
Consider a simplified example that appears almost everywhere: summing eligible line items based on some predicate. Perhaps you’re calculating a partial total for items that qualify for a specific promotion, or accumulating a shipping surcharge for certain dimensions, or just tracking the number of items that meet a criterion.
Imperative code typically starts by declaring a mutable accumulator and then stepping through the collection, mutating the accumulator as it goes:
int total = 0;
for (Item item : items) {
if (item.isEligible()) {
total += item.getAmount();
}
}
return total;
Nothing here is surprising. Java encourages us to write code this way. But observe what’s actually happening:
You create a variable that temporarily represents a truth.
You then mutate that truth as you step through the loop.
You must remember the original intent of the accumulator, its type, its update rules, and the conditions under which the update happens.
You must ensure the update is correct in every branch.
You must ensure the loop termination matches your intent.
You must ensure the initial value was correct.
The code is short, but the cognitive burden is not. Your mind must simulate the progression of state—line by line, iteration by iteration. This is the essence of imperative programming: the code describes a timeline, and your brain must follow it.
Small as this example is, its structure invites familiar mistakes: off-by-one errors, missed conditions, incorrect initial values, branch mutations that never occur because of a misplaced continue, or updates performed in the wrong loop. None of these are exotic failures; they are routine.
And they persist because the structure of the code permits them.
The Functional Pivot: Code as a Value
Now consider the functional equivalent. Instead of describing how to accumulate a value over time, we express the value itself as a composition of smaller expressions:
items
.filter(_.isEligible)
.map(_.amount)
.sum
This is not merely more compact. It is differently structured. There is no timeline, no accumulator to track, no place where a state may be updated incorrectly, no branch where a mutation might be skipped. The logic describes the relationship between the input and the output, not the process that transforms one into the other.
This matters because the structure eliminates entire categories of bugs:
You cannot forget to update the accumulator; there is none.
You cannot update it in the wrong branch; there is none.
You cannot produce an off-by-one result; there is no counter.
You cannot mis-sequence operations; the expression is declarative.
Instead of describing actions, you describe intent.
Developers often describe this moment as the point where the code stops feeling like “instructions for a machine” and starts feeling like “a description of a relationship.” And once the structure shifts, the cognitive load drops. You no longer mentally execute the loop; you read the expression and understand its evaluation.
Read More: Why Functional Programming Matters for the Systems We Build Today
From Updating State to Transforming Data
Suppose you’re computing the total discountable amount for items that meet a promotion’s eligibility criteria. The imperative version, again, starts with mutation:
Money eligible = Money.zero();
for (CartItem item : cart.getItems()) {
if (promotion.appliesTo(item)) {
eligible = eligible.plus(item.getPrice().multiply(item.getQuantity()));
}
}
return eligible;
Functional style:
cart.items
.filter(promotion.appliesTo)
.map(item => item.price * item.quantity)
.foldLeft(Money.zero)(_ + _)
Functionally, they compute the same thing. But architecturally, they inhabit different worlds.
In the imperative version, the logic lives partly in the loop, partly in the condition, partly in the mutation, and partly in the return. In the functional version, the logic appears as a single expression representing the value you want.
When a future engineer modifies this logic—because promotions now have tiered applicability, or discountable amounts must account for a customer’s loyalty tier, or some items can only be discounted once per brand—they modify an expression, not a process. They do not need to preserve the integrity of an accumulator scattered across branches; they extend a composition of transformations. This is how functional programming reduces the cost of change.
Why Expressions Reduce Cognitive Load
In imperative code, you must reconstruct the intermediate states in your mind. This mental simulation is often invisible, but it is real, especially in systems with branching, early returns, nested loops, or complex eligibility rules.
When you write in expressions, the intermediate states are not hidden; they do not exist. The computation is the value, and the value is expressed structurally.
The structure itself grants clarity:
Filtering says: “These items matter.”
Mapping says: “This is the property we need.”
Folding says: “This is how the values combine.”
There is nothing else. No hidden state. No mutation timeline. No branching that alters state in surprising ways.
The code becomes easier to test, easier to reason about, easier to extend, and easier to parallelize. Which brings us to a less obvious but important consequence.
Expressions Compose; Statements Accumulate
One of the most underestimated benefits of expression-oriented thinking is composability. Expressions can be nested, passed around, combined, transformed, deferred, or executed lazily. They can describe:
promotion chains,
eligibility structures,
shipping pipelines,
tax calculations,
pricing transformations,
or any other flow that can be expressed as a relationship between data and a result.
Statements, by contrast, accumulate. Their meaning depends on history—on the order in which actions happened. Composability suffers because statements are “things that happened,” not “values that exist.”
This difference is not philosophical; it is architectural. Systems built from expressions scale differently—both in code size and conceptual complexity—because the pieces fit together predictably.
A Commerce Example: Repricing Eligibility
Because the series now uses commerce as its reference domain, here is an example where expression-oriented thinking produces a meaningfully different architecture.
Suppose you want to compute the set of items eligible for a mid-cart promotion—say, “20% off when at least three qualifying items are present.” Imperative code often starts with counters, flags, and mutation:
List<CartItem> eligible = new ArrayList<>();
int count = 0;
for (CartItem item : cart.getItems()) {
if (promotion.appliesTo(item)) {
eligible.add(item);
count++;
}
}
if (count >= 3) {
return eligible;
} else {
return Collections.emptyList();
}
Functional expression:
val eligible = cart.items.filter(promotion.appliesTo)
if (eligible.size >= 3) eligible else List.empty
The functional version is not only smaller; it is structurally closer to the business rule:
Identify eligible items.
Apply a threshold.
Return the result.
There is no counter to initialize, update, or maintain. No branching that alters state. No risk of returning a partially mutated accumulator. The code reflects the domain.
Once you shift to expressions, this architecture becomes the natural one, not the forced one.
Statements Produce Behavior; Expressions Produce Meaning
Thinking in statements is natural because most languages teach it first. But thinking in expressions is liberating because it realigns programming with problem-solving rather than instruction-giving.
Expressions:
evaluate to values,
compose cleanly,
eliminate implicit state,
remove entire classes of bugs,
and reduce the cognitive load of understanding the system.
This shift is not about shorter code, although the code often becomes shorter. It is about structural clarity—the kind that persists as the system grows.
In the next article, we will extend this mindset shift by exploring the higher-order functions—map, filter, and flatMap—that replace imperative looping patterns. If expressions help us stop thinking in timelines, these core functional operators help us stop thinking in mechanics. Together, they make iteration implicit, intent explicit, and business rules dramatically clearer.