Go back

Why I Love Functional Programming in C++, TypeScript, and Python

Published:

Why I Love Functional Programming

Table of Contents

Open Table of Contents

Introduction

Functional programming isn’t a new trend – its roots reach back to the 1950s. As one practitioner noted, “functional programming is not a new idea; in fact, Lisps go back to like the 1950s or 1960s”. Lisp itself was first sketched out in 1958 as a mathematical language influenced by Alonzo Church’s lambda calculus. Since then, the ideas of pure functions and immutability have matured, but surprisingly many codebases still cling to older models. In my experience, those older paradigms – procedural “step-by-step” scripts or heavy object-oriented class hierarchies – often lead to tangled, stateful code. Modern C++, TypeScript, and Python are all multi-paradigm, and they’ve been quietly evolving powerful functional features. In this article I’ll explain why small, pure functions (with minimal global dependencies) can dramatically improve code quality. I’ll give concrete examples in C++, TypeScript, and Python, share anecdotes from industry, and offer tips and callouts for thinking functionally. Along the way we’ll note caveats (compiler quirks, portability issues, etc.) and specific gotchas in each language.

The Pains of Procedural and Object-Oriented Code

Before diving into functional programming, it’s worth acknowledging why many of us find procedural or object-oriented (OO) code frustrating. In purely procedural code, we often write long sequences of steps operating on shared state. Global variables and mutable state abound, and it becomes easy to lose track of side effects. As one analysis bluntly puts it, procedural code “is not reusable” and can become insecure or unscalable as it grows. Procedures tend to bake in order-of-execution, so managing complexity requires careful discipline. I’ve seen projects where adding new features meant carving out yet another global function, leading to code duplication and surprising bugs.

In object-oriented code, we bundle data and behavior into classes, which can improve modularity—but this comes with trade-offs. Designing the right class hierarchy is hard, and over time we often end up with large inheritance trees or tightly coupled components. One developer quipped that OOP can feel like trying to model everything in the “real world,” leading to a steep learning curve and bulky programs. Classes encourage hiding state in objects, but that can make understanding program flow tricky: actions get distributed across methods and objects, and understanding how everything interacts at runtime is hard. Worse, objects can end up sharing mutable state (e.g. modifying fields on passed objects), making bugs appear in distant code.

Even smart OO languages have their pitfalls. For example, Java famously has tangled issues with inheritance and type hierarchies. Python’s dynamic objects can be mutated by any function that holds a reference. And C++ allows mixing free functions, classes, and globals in confusing ways. In every case, implicit dependencies on external or global state can lead to unpredictability. To borrow an analogy, one reviewer said pure functions behave like mathematical functions: give them x and y, you get f(x,y) – period. But methods tied to objects might as well depend on a mysterious “state” that lives elsewhere. In fact, a pure function must always return the same output for the same input and cause no side effects on the outside world. Violating this principle is what introduces bugs and complexity.

Caution: Procedural routines and object methods often rely on hidden state. If you see a function depending on global or mutable data, its behavior could change unexpectedly. Keeping functions small and self-contained avoids these traps.

The functional style flips the script: we structure programs as a composition of pure functions and immutable data. Instead of telling the computer how to do each step imperatively, we declare what transformations to apply to our data. This declarative approach makes code more transparent. Without hidden state, we can reason about each piece independently. As the Linode docs summarize: functional programs are “designed around predictable functions that avoid side effects,” which makes them naturally suited to concurrency and easier testing. In my own work, switching to tiny functions that take all needed inputs as parameters (rather than grabbing globals) made it far simpler to write unit tests and debug problems. One colleague remarked that bugs in her codebase “evaporated” after refactoring into pure functions; when everything depends only on inputs, I believe her claim.

The Power of Pure Functions and Immutability

Why do pure, small functions improve code quality? It comes down to predictability, modularity, and safety. First, predictability: a pure function always returns the same result given the same inputs (by definition). There are no mysterious dependencies on outside state or ordering, so you can mentally replace the function call with its output. This makes reasoning and debugging much easier. As one author notes, pure functions grant referential transparency – a function call can be swapped for its value without changing program behavior.

Second, testability and reusability: because pure functions don’t rely on hidden state, we can test them in isolation. We don’t need to spin up a complicated environment or mock a database; we just call the function with sample inputs. In practice, this decoupling means each function becomes a tiny unit of guaranteed behavior. It’s straightforward to test all combinations of inputs or to reuse a pure function in multiple contexts. For example, I once refactored a forecasting module by breaking it into many small transformer functions. As a result, I could reuse a “normalize data” function across two projects, with confidence it would never mutate the source array or rely on global flags. Hilary Oba summarizes this well: “Pure functions are inherently testable… [and] highly modular and reusable.”

Third, concurrency and parallelism: without shared mutable state, we eliminate race conditions. Functional code often processes collections with stateless operations like map/reduce. Each element can be handled independently. The same Linode guide highlights this: because functional functions “avoid side effects” they “lend to concurrent operations”. In practice, I’ve seen this in C++ when using std::transform or parallel ranges; threads can each run the same lambda on different chunks without locks. In JavaScript/TypeScript, using immutable data and pure functions pairs nicely with asynchronous promises or web workers, since callbacks won’t stomp on each other’s state. Even Python’s multiprocessing tends to work better with pure code because there’s no fiddling with one shared dictionary.

Finally, readability and maintenance: once we commit to pure functions, code becomes a chain of clear transformations. Instead of a block of lines with changing state, we end up with data flows. This is why many modern C++ libraries and JS frameworks encourage function chaining. As a benefit, new team members often find functional-style pipelines “clearer, more readable” because each step is obvious. For example, I used to work with nested loops modifying lots of variables. After refactoring into map() and filter() calls (even in Python list comprehensions), the code shrunk by half and a reviewer literally smiled, saying “this is way easier to follow”. In summary, small pure functions isolate change and simplify reasoning, and that directly leads to higher-quality code.

Functional Patterns Emerge in C++

C++ is often associated with low-level, imperative code, but modern C++ (11/14/17/20+) has steadily added functional tools. As Rainer Grimm observes, C++20 even makes function composition and lazy evaluation first-class citizens via the ranges library. Here are some key points in C++ evolution:

A simple example highlights C++ FP style:

#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>

int main() {
    std::vector<int> data {1, 2, 3, 4};
    // Compute squares with a lambda
    std::vector<int> sq(data.size());
    std::transform(data.begin(), data.end(), sq.begin(),
                   [](int x){ return x*x; });
    // Sum them with std::accumulate
    int sum = std::accumulate(sq.begin(), sq.end(), 0);
    std::cout << sum << std::endl;  // prints 30
}

Here each step is a pure function (transform and accumulate), not mutating shared state in a tricky way. The code is concise and testable.

The C++ Standard Library also includes functional helpers like std::invoke, std::apply, and <functional> for std::bind, and newer features like pattern matching are on the horizon (C++23/C++26 get a std::visit-style std::variant match, plus customizable concepts). These show a clear trend: C++ is bolting on more declarative, FP-friendly constructs.

Note: C++ is still not a pure functional language, but it’s multi-paradigm. You can sprinkle FP techniques into C++ as needed. The benefit is better code clarity and fewer bugs from side effects.

That said, some C++ caveats apply:

Despite these gotchas, adopting FP in C++ has major payoffs. In one project I led, we refactored a data pipeline by replacing manual loops and state mutations with std::views::transform and lambdas. The code became shorter and parallel-friendly, and our profiler showed it was just as fast. In an embedded systems project, writing pure computation functions (with constexpr) dramatically simplified unit testing; we could compile parts of the code for a quick check without running hardware.

Functional Patterns in TypeScript

TypeScript (and JavaScript) was originally a mix of procedural and prototype-based object patterns, but it has grown very functional-friendly. JavaScript treats functions as first-class citizens (you can pass them around as data), and TypeScript inherits that. Here are some highlights:

Here’s an example in TypeScript showing function composition and pure data flow:

type Data = { value: number };
const input: Data[] = [{value:1},{value:2},{value:3}];

// A pure function to transform Data
function toDouble(d: Data): number { return d.value * 2; }

// Use Array.map to apply it
const doubles = input.map(toDouble);  // [2,4,6]

// Sum them functionally
const total = doubles.reduce((acc, v) => acc + v, 0);
console.log(total);  // 12

Notice toDouble has no side effects; we never mutate input or any external variable. The array methods keep everything expressive. In a real TypeScript project, we’d add types and maybe use ReadonlyArray<Data> to ensure input stays immutable. But even without fancy typing, this FP style is straightforward.

Tip: In TypeScript, favor pure functions and const declarations. Use arrow functions for brevity. For example, you can chain operations:

const result = data
  .filter(x => x.active)
  .map(x => x.value * 2);

This pipeline reads almost like English: filter by active, then double the values. It’s clear and avoids mutating any data element in place.

However, some TypeScript/JavaScript quirks must be noted:

Despite these caveats, the advice for TS is clear: think in terms of immutable transformations. In practice I’ve seen teams drastically reduce bugs by using .filter()/.map() instead of manual for loops that push or mutate. One colleague refactored a state-management module into a series of pure reducers and saw state bugs disappear, echoing Redux’s principles. Another found that writing UI-render functions as pure components simplified testing: you just call the function with props and check the output.

Python: Embracing Functional Parts of a Scripting Language

Python is famously multi-paradigm and originally imperative, but it has long supported functional techniques too. Its creator, Guido van Rossum, added features based on user demand. For example, Python didn’t even have an anonymous lambda until 1994 – early adopters had to use exec hacks! But since then, Python has acquired:

Here’s a Python example showing pure transformation:

def double_list(data):
    # Pure function: no side effects, returns new list
    return [x*2 for x in data]

orig = [10, 20, 30]
result = double_list(orig)     # [20, 40, 60]
print(orig)   # [10, 20, 30] – original list untouched

This simple use of a list comprehension yields a new list, leaving orig intact (immutable view). In contrast, a loop-based function that did for i in range(len(data)): data[i] *= 2 would mutate data.

A fun anecdote from Python’s history illustrates how FP came in: in 1994, map/filter/reduce and lambda were added to Python’s core in one go because users asked for the Lisp-style pipeline operations. However, even then Guido noted many found Python’s lambda semantics limited (no closures until later). By Python 3, reduce() was demoted to functools.reduce because list comprehensions and loops took over. Nevertheless, the language never removed these FP tools entirely.

Example: Python’s map() was designed for FP but often we use list comprehensions instead. Both are pure in intent. For large-scale transformations, consider generators to save memory:

import math
def distances(points):
    # yields distance of each point from origin (pure generator)
    return (math.hypot(x,y) for (x,y) in points)

Here distances() produces values lazily and never alters the input list.

Still, Python’s quirks merit attention:

Adopting FP in Python tends to play nicely with the language’s style. In one project I worked on, our data-validation code had countless if-statements and flags. I rewrote it as a sequence of small filter/map passes on the data list, each function handling one validation rule. The code became shorter, and errors (like an empty list vs None check) were isolated to individual functions, which was easy to unit-test. This transformation was so effective that the team embraced even more FP-ish design, like using map() on error messages and only resorting to loops for trivial tasks.

Tips for Thinking in a Functional Style

Switching mindsets to functional programming can be challenging. Here are some concrete tips that have helped me (and colleagues) think more functionally:

💡 Pro Tip: A helpful mindset shift is to treat functions like data. You can store them in variables, pass them around, and combine them. In TypeScript, try writing const f = (x: number) => x * 2; and then use it in multiple contexts. In C++, use std::function or auto lambdas. This reminds you that functions themselves are first-class in FP.

By practicing these habits, FP thinking becomes natural. The goal is not to blindly avoid for-loops or classes, but to use them sparingly and favor small, composable units. Over time, you’ll find codebases where bugs can be traced to one flawed pure function rather than tangled side effects.

Comparing Languages: Functional Features at a Glance

Below is a high-level comparison of how C++, TypeScript, and Python support key functional concepts. This illustrates each language’s functional tools and limitations:

FeatureC++TypeScript/JavaScriptPython
First-Class FunctionsYes (since C++11 lambdas and std::function)Yes (all functions; arrow =>)Yes (def, lambda)
Anonymous FunctionsLambdas (C++11+), generic lambdas (C++14+)Arrow functions (() => { })lambda (single expression)
Built-in Map/Filter/ReduceAlgorithms: std::transform, std::accumulate, <algorithm> functionsArray methods: .map(), .filter(), .reduce()map(), filter(), functools.reduce(), list/generator comprehensions
Immutability Supportconst, constexpr, immutable containers (e.g., std::string, C++23 std::span readonly)const variables, readonly types, use of Immutable.js/Ramda etc.tuple, frozenset, no built-in enforced immutability, but encourage no mutation
Pattern Matching/ADTsstd::variant + visitor; C++23 pattern matching (preview)No native pattern matching (ESNext stage); libraries like ts-pattern existmatch/case (Python 3.10+) for destructuring; no real ADT (except classes)
Tail CallsNo (no guaranteed TCO)No (most engines omit TCO)No (Python doesn’t optimize tail calls)
Lazy EvaluationC++20 Ranges (lazy views)Laziness only via custom generators or RxJSGenerators (yield) provide lazy sequences
Functional LibrariesBoost.Hana, range-v3, functional extensionsRamda, lodash/fp, fp-ts, RxJSfunctools, itertools, third-party libs like toolz

This table shows that all three languages have embraced functional features to varying extents. In each case, pure functions are possible and often the easiest to reason about.

Real-World Anecdotes

Let me share a few stories from practice, some from my own projects and others from peers, about the impact of going functional:

Each of these anecdotes illustrates a common theme: when the team isolated functions from side effects, they gained clarity and fewer bugs. It can be hard to refactor old code, but starting new modules with FP thinking pays off quickly.

Language-Specific Advice and Gotchas

When adopting FP in each language, watch out for language quirks. Here are some gotchas and tips per language:

Tip: A quick sanity check in any language is to ask, “Does this function look at any global variable or static mutable data?” If yes, consider passing that data in instead. Making dependencies explicit is a core FP habit.

Conclusion

Functional programming is not just an academic idea; it has practical benefits in the wild. By focusing on small, pure functions, we create code that is predictable, testable, and modular. C++, TypeScript, and Python have all been drifting toward more functional capabilities – from C++ lambdas and ranges, to TS/JS arrow functions and React components, to Python’s comprehensions and tools. In each language, embracing even parts of the functional paradigm can dramatically improve high-performance, high-quality code.

In my journey, adopting FP thinking transformed how I approach problems. I encourage you to start small: try rewriting one function in a pure way, or replace a loop with a map/filter. Use the tips and examples here to guide you. You may find, as I and many others have, that once you shed the extra baggage of mutable state, your code – whether in C++, TS, or Python – becomes cleaner and more robust.

References: Concepts and quotes were drawn from language documentation and expert writings. For instance, Lisp’s roots are noted by its inventor John McCarthy. Tutorials and blog posts detail the advantages of FP and examples in these languages. Wherever possible, authoritative sources (including Wikipedia and programming guides) have been cited to back up these points.


Suggest Changes

Previous Post
What is C++? A Deep Dive into the Language That Powers Performance-Critical Software
Next Post
Robust Streaming Statistics: When Every Byte Counts