Functional Programming in C++

Functional Programming in C++

Chapter 1: Introduction to Functional Programming

This chapter introduces functional programming (FP) as a paradigm emphasizing what a program should achieve rather than how. It contrasts imperative (step-by-step) and declarative (result-focused) styles using examples like counting lines in files . Key concepts include pure functions (no side effects, same input = same output) and lazy evaluation. The evolution of C++ as a multiparadigm language is highlighted, noting how C++11/14/17 features (lambdas, ranges) enhance FP capabilities. A key takeaway: FP reduces mutable state issues, improving concurrency safety, as seen in the text editor example where immutable data copies prevent corrupted saves .

Chapter 2: Getting Started with Functional Programming

Focused on higher-order functions (HOFs), this chapter demonstrates how HOFs like std::accumulate and std::transform abstract iteration logic. For instance, std::accumulate can sum values or count newlines via custom folding functions . The chapter also highlights STL algorithms’ composability issues—intermediate collections are often needed, unlike in functional languages. A hands-on example shows filtering females from a list using std::copy_if with a lambda, then transforming to names with std::transform . Recursion and tail-call optimization are briefly explored, though loops and folds are preferred for efficiency.

Chapter 3: Function Objects

This chapter dissects C++ constructs that behave like functions: regular functions, function pointers, lambdas, and functor classes. Lambdas are shown as syntactic sugar for anonymous functors, capturing variables by value/reference or using C++14’s generic auto for type flexibility . Examples include creating a greater_than functor to filter ages and using std::function to wrap callables, though with performance caveats. The Boost.Phoenix library is introduced for terse function composition, like arg1 <= 42 to create predicates . Key takeaway: Lambdas and functors offer concise, type-safe ways to pass behavior to algorithms.

Chapter 4: Creating New Functions from Old Ones

This chapter explores techniques to derive new functions from existing ones. Partial function application uses std::bind to fix arguments, turning binary functions like std::greater into unary predicates (e.g., is_greater_than_42 = std::bind(std::greater<double>(), _1, 42)). Currying transforms multi-argument functions into chains of unary functions, as seen in converting print_person into a curried version that accepts arguments sequentially. Function composition allows nesting operations, like filtering and transforming strings in a pipeline. For example, words | filter(is_female) | transform(name) succinctly processes data. These methods enhance reusability and reduce boilerplate, though std::bind may introduce performance overhead compared to lambdas.

Chapter 5: Purity and Avoiding Mutable State

Mutable state introduces concurrency issues and bugs, as seen in a movie_t class where concurrent score updates cause undefined behavior. Pure functions, defined by referential transparency, ensure the same output for the same inputs, avoiding side effects. For instance, a pure max function without logging is reliable. C++’s const keyword enforces immutability, but mutable allows internal state changes in const methods, like caching employment history with mutexes. Immutable data structures, though memory-intensive, prevent shared state issues, contrasting with mutable designs prone to race conditions.

Chapter 6: Lazy Evaluation

Lazy evaluation delays computation until needed, optimizing performance. The lazy_val class caches results of pure functions, like memoizing Fibonacci calculations to avoid redundant work. Expression templates defer string concatenation, avoiding intermediate copies: fullname = lazy_concat + surname + ", " + name builds the result in one pass. Lazy sorting processes only necessary elements, like fetching the top 10 items without sorting the entire collection. This approach shines in UI scenarios, loading data (e.g., employee photos) only when visible, reducing memory and CPU usage.

Chapter 7: Ranges

Ranges address STL’s iterator pair limitations by encapsulating collections, enabling lazy composition with a pipe syntax (|). For example, people | filter(is_female) | transform(name) cleanly filters and transforms data without intermediary variables. Views (e.g., filter, transform) create lightweight proxies, while actions (e.g., sort, unique) modify data eagerly. Infinite ranges, like view::ints(1), pair with finite ranges for indexing, and sentinels handle open-ended data (e.g., input streams). Ranges simplify complex tasks like word frequency counting, replacing loops with declarative pipelines.

Chapter 8: Functional Data Structures

Immutable data structures, like linked lists and bitmapped vector tries, enable safe data sharing. Linked lists allow O(1) prepends/removals by sharing nodes, but appending requires copying. Bitmapped vector tries (inspired by Clojure) use tree-like structures with fixed-size chunks, enabling O(1) appends/accesses by sharing most nodes. For example, appending to a trie only copies the path to the new element, not the entire structure. These structures trade slight cache inefficiency for persistent, immutable data handling, ideal for functional programming’s copy-heavy workflows.

Chapter 9: Algebraic Data Types and Pattern Matching

Algebraic data types (ADTs) model states using sum types (e.g., std::variant) and product types (e.g., structs). For example, modeling a tennis game state with std::variant avoids invalid states like “40-40” being treated as a numeric score. Sum types like std::optional and expected<T, E> handle errors explicitly, replacing null pointers or exceptions. Pattern matching, via libraries like Mach7 or std::visit, simplifies handling ADTs. For instance, parsing JSON with std::variant and matching against valid types ensures type-safe error handling, as seen in extracting user data from structured inputs.

Chapter 10: Monads

Monads extend functors by enabling composition of functions that return wrapped values (e.g., std::optional, futures). For example, chaining user_full_name and to_html with mbind handles potential failures in both functions, returning an expected value or error. Reactive streams, modeled as monads, process asynchronous data flows—like a web service receiving client messages and transforming them with transform and filter. Monads like future allow non-blocking operations, where then chains asynchronous tasks, such as fetching a URL and parsing its content without halting the main thread.

Chapter 11: Template Metaprogramming (TMP)

TMP manipulates types at compile time, enabling compile-time checks and code generation. For example, contained_type_t deduces the element type of a collection using decltype and std::remove_reference_t, avoiding runtime errors. Metafunctions like is_same and remove_reference use template specialization to conditionally return types. TMP also powers currying, where a generic curried class wraps functions, allowing partial argument application: print_person_cd(martha)(std::cout)(format) dynamically builds function calls at compile time.

Chapter 12: Functional Design for Concurrent Systems

The actor model promotes isolated components communicating via messages, avoiding shared mutable state. Actors like a web service listener process messages as reactive streams, using transformations like filter to reject invalid inputs (e.g., empty messages). Reactive streams, treated as monads, enable async processing: a server can transform client messages (e.g., parsing JSON to bookmarks) and reply asynchronously. For example, a throttle actor uses mutable state to limit client messages, ensuring fairness without locks, as each actor processes messages sequentially in its own thread.

Chapter 13: Testing and Debugging

Pure functions simplify unit testing by eliminating side effects—e.g., a sum function with no state can be tested with fixed inputs and outputs. Automatic test generation uses property-based testing to validate edge cases, like ensuring a sorting function works for all permutations. For concurrent systems, testing monad-based streams involves verifying message ordering and error handling. For example, testing a reactive stream that parses JSON ensures valid inputs produce correct outputs and invalid ones trigger errors, using expected to track failures explicitly. Static assertions and compile-time checks (e.g., static_assert) catch type mismatches early, preventing runtime bugs.