why pure code changes how you think about software

why pure code changes how you think about software

Most programming starts with a simple question: how do I make the computer do this?

Functional programming asks a slightly different question: how do I make this program easier to reason about?

That shift sounds small, but it changes almost everything. Instead of building software around mutation, hidden state, and step-by-step instructions, functional programming pushes you toward pure functions, immutable data, explicit errors, and composable abstractions.

The main lesson is this: good functional code makes the hard parts of programming visible.

pure functions are the foundation

At the center of functional programming is the pure function.

A pure function always gives the same output for the same input, and it does not secretly change anything outside itself. It does not mutate global variables. It does not print to the console. It does not read from a file. It does not call an API. It does not depend on the current time. It simply takes input and returns output.

For example, this is pure:

def add(x: Int, y: Int): Int = x + y

Every time you call add(2, 3), you get 5.

That might seem too obvious to matter, but the benefit becomes clearer as programs get larger. Pure functions are easier to test because there is no hidden setup. They are easier to debug because there are fewer invisible causes. They are easier to reuse because they do not depend on some messy external environment.

Impure code can work, but it often forces you to mentally track everything around it. Pure code shrinks the amount of context you need to understand a program.

referential transparency makes code easier to reason about

One of the most critical ideas is the concept of referential transparency.

An expression is referentially transparent if you can replace it with its result without changing the behavior of the program.

For example:

val result = add(2, 3)

If add(2, 3) is pure, then you can replace it with:

val result = 5

Nothing changes.

That is powerful because it means you can reason about code almost like algebra. You do not have to ask, “what else happened when this function ran?” You only have to ask, “what value does this expression produce?”

This is one of the biggest mental shifts in functional programming. In ordinary imperative code, a function call might change a database, update a variable, throw an exception, log something, or trigger some hidden effect. In functional code, the goal is to make those effects explicit instead of burying them inside ordinary-looking function calls.

immutability reduces accidental complexity

Functional programming also favors immutable data.

In an imperative style, you often create a variable and then update it repeatedly:

var total = 0
total = total + 5
total = total + 10

In functional programming, you avoid changing existing values. Instead, you create new values from old ones:

val total1 = 0
val total2 = total1 + 5
val total3 = total2 + 10

At first, this can feel inefficient or awkward. But the design benefit is huge: once a value exists, it does not unexpectedly change.

Mutation creates a common source of bugs. You pass data to one part of the program, another part modifies it, and suddenly the original code behaves differently. This gets even worse in concurrent programs, where multiple threads may touch the same data.

Immutability removes that entire category of problems. It makes data safer to share, easier to inspect, and easier to reason about.

higher-order functions replace repetitive loops

Functional programming also treats functions as values. You can pass functions into other functions, return them from functions, and combine them.

This leads to higher-order functions like:

map
filter
fold
flatMap

Instead of manually writing loops, you describe transformations.

For example, instead of saying:

var result = List[Int]()
for (x <- numbers) {
  if (x % 2 == 0) {
    result = result :+ (x * 2)
  }
}

You can write:

val result = numbers.filter(_ % 2 == 0).map(_ * 2)

The second version is shorter, but the real benefit is not just fewer lines. It makes the programmer’s intention clearer.

You are not telling the computer every mechanical step. You are saying: keep the even numbers, then double them.

Functional programming often turns code from a sequence of commands into a pipeline of transformations.

errors should be values, not surprises

Another major idea is that errors should be represented explicitly.

In many languages, errors are handled through exceptions. Exceptions are useful, but they are also invisible in the type signature. A function may look like it returns a value, but it might actually throw an exception instead.

Functional programming often prefers types like:

Option[A]
Either[E, A]

Option[A] means a value might exist or might not.

Some(value)
None

Either[E, A] means a computation can fail with an error of type E or succeed with a value of type A.

Left(error)
Right(value)

This makes failure part of the program’s structure. The type itself tells you that something might go wrong.

That is useful because it forces the programmer to handle the failure case. Instead of hoping nothing breaks, the code has to account for missing values, invalid input, or failed computations.

This is one of the practical strengths of functional programming: it moves problems from runtime surprises into compile-time design.

laziness lets programs delay work

Laziness.

A lazy computation is not evaluated until its result is needed. This allows you to describe large, expensive, or even infinite computations without running them all at once.

For example, a lazy stream could represent an infinite sequence of numbers. You are not storing every number in memory. You are describing how to produce the next value when needed.

This is useful for data pipelines. You can define a chain of transformations, but only compute the pieces required by the final result.

Laziness separates describing a computation from executing it immediately. That gives the programmer more control over performance and structure.

even state can be handled functionally

One common misunderstanding is that functional programming cannot handle state.

It can. It just handles state explicitly.

In imperative programming, state is often changed in place:

state = newState

Functional programming models state transitions as functions. A stateful computation can be represented like this:

State => (Result, State)

In other words, take an old state, produce a result, and return a new state.

Nothing is mutated. The change is represented as a value.

This pattern is especially important for things like random number generation, simulations, interpreters, and workflows. The program still evolves over time, but every transition is visible.

That makes the code easier to test. You can pass in a known state and check whether the function returns the expected result and next state.

functional programming builds small languages

Not full programming languages, but small sets of composable operations for a specific domain.

Examples like:

parallel computation
property-based testing
parsers
stream processing

The pattern is similar each time. Instead of solving one specific problem directly, you create a small vocabulary of operations. Then complex behavior can be built by combining simple pieces.

This is where functional programming becomes more than “avoid mutation.” It becomes a design method.

You are not just writing functions. You are designing abstractions that let programs be assembled cleanly.

the scary words are really about composition

Functional programming has a reputation for intimidating terminology:

monoid
functor
applicative
monad

These words can make the subject feel more abstract than it needs to be.

At a practical level, these concepts are about composition. They describe common patterns for combining values, computations, effects, or structures.

A monoid is about combining things with an identity value.

A functor is about mapping over a structure.

An applicative is about combining independent computations inside a structure.

A monad is about sequencing dependent computations inside a structure.

The names are abstract, but the purpose is practical. These patterns help programmers write reusable code that works across many different contexts.

For example, Option, Either, List, and IO may seem like different things. But they can share similar operations, like map and flatMap, because they follow similar patterns.

That is the payoff of abstraction: once you recognize the pattern, you can reuse the same way of thinking across different problems.

effects should be pushed to the edges

Functional programming does not mean avoiding real-world effects.

Programs still need to read files, call APIs, write to databases, print output, and interact with users.

The difference is that functional programming tries to separate the description of effects from their execution.

Instead of mixing pure logic and side effects everywhere, you keep most of the program pure and push effects to the outer edges.

This creates a cleaner architecture:

core logic: pure, testable, predictable
outer shell: input/output, databases, APIs, user interaction

That separation makes programs easier to test. You can test the core logic without needing a real database or network call. You can also change the outer system without rewriting the inner logic.

This is one of the most valuable lessons from functional programming, even for people who do not write purely functional code.

why this matters

The real value of functional programming is not that it makes code shorter. Sometimes it does. Sometimes it does not.

The real value is that it makes code more honest.

It makes hidden assumptions visible. It turns failure into a type. It turns state changes into explicit transitions. It turns side effects into controlled descriptions. It turns mutation into new values. It turns repetitive control flow into reusable patterns.

That matters because most software problems are not caused by computers being bad at following instructions. They are caused by humans losing track of complexity.

Functional programming is a discipline for reducing that complexity.

conclusion

The biggest message is that programs become easier to understand when their behavior is explicit. Pure functions, immutable data, referential transparency, typed errors, lazy evaluation, and composable abstractions all serve the same goal: making software easier to reason about.

Scala is just the vehicle. The deeper lesson applies far beyond Scala.

Functional programming is not about making code look clever. It is about making code behave predictably.

And in large systems, predictability is everything.