← Back to DevBytes

Functional Programming in C

What is Functional Programming in C?

Functional programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions, emphasizing immutable data, pure functions, and declarative style over imperative step-by-step mutation. While C is not a functional language by design, it provides enough low-level mechanisms—function pointers, structs, macros, and disciplined conventions—to implement a surprisingly expressive functional style. Writing functional C means structuring your code so that functions are first-class entities, side effects are minimized or isolated, and data transformations are composed from smaller, reusable building blocks rather than scattered loops and mutable state.

Why Functional Programming Matters in C

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

1. Predictability and Testability

Pure functions—those that depend only on their explicit inputs and produce outputs without altering external state—are trivial to test. You can call them with known arguments and assert on the return value, with no hidden dependencies to mock or global state to reset.

2. Composability and Reuse

When you design functions as small, single-purpose transformations, you can snap them together like Lego bricks. A map that applies a function to every element of an array can be composed with a filter and a reduce to form a complete data pipeline, each stage being independently swappable and testable.

3. Concurrency Safety

Immutable data and pure functions naturally avoid race conditions. If no function mutates shared state, threads can execute independently without locks or synchronization, which is a major advantage in embedded systems and high-performance server code where C is often deployed.

4. Code Clarity

A functional style forces you to separate "what" from "how." The high-level intent—transform this collection, keep only these elements, aggregate the result—stays visible at the top level, while the gritty details live in small, named helper functions.

Core Concepts and How to Implement Them in C

First-Class Functions via Function Pointers

The cornerstone of functional C is the function pointer. A function pointer stores the address of a function and allows it to be passed as an argument, returned from another function, or stored in a struct. This gives functions "first-class" status—they can be manipulated at runtime like any other value.

#include <stdio.h>

// Typedef to make function pointer syntax readable
typedef int (*transform_fn)(int);

// A concrete transformation function
int double_val(int x) {
    return x * 2;
}

int square_val(int x) {
    return x * x;
}

// A function that accepts another function as an argument
void apply_to_array(int *arr, size_t len, transform_fn fn) {
    for (size_t i = 0; i < len; i++) {
        arr[i] = fn(arr[i]);
    }
}

int main(void) {
    int numbers[] = {1, 2, 3, 4, 5};
    size_t count = sizeof(numbers) / sizeof(numbers[0]);

    // Pass double_val as a function pointer
    apply_to_array(numbers, count, double_val);
    for (size_t i = 0; i < count; i++) {
        printf("%d ", numbers[i]);  // prints: 2 4 6 8 10
    }
    printf("\n");

    // Pass square_val instead
    apply_to_array(numbers, count, square_val);
    for (size_t i = 0; i < count; i++) {
        printf("%d ", numbers[i]);  // prints: 4 16 36 64 100
    }
    printf("\n");

    return 0;
}

Note the typedef—it eliminates the cryptic int (*)(int) syntax from parameter lists and makes the code far more readable. Always typedef your function pointer signatures.

Higher-Order Functions: map, filter, reduce

Higher-order functions either take functions as arguments or return new functions. The classic trio—map, filter, reduce—can be implemented in C to transform collections declaratively.

Map — Transform Every Element

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

typedef int (*map_fn)(int);

// Allocates a new array with transformed values
int *map(const int *src, size_t len, map_fn fn) {
    int *dest = malloc(len * sizeof(int));
    if (dest == NULL) return NULL;
    for (size_t i = 0; i < len; i++) {
        dest[i] = fn(src[i]);
    }
    return dest;  // Caller must free
}

int increment(int x) { return x + 1; }

int main(void) {
    int original[] = {10, 20, 30, 40};
    size_t len = sizeof(original) / sizeof(original[0]);

    int *result = map(original, len, increment);
    if (result) {
        for (size_t i = 0; i < len; i++) {
            printf("%d ", result[i]);  // prints: 11 21 31 41
        }
        printf("\n");
        free(result);
    }
    return 0;
}

The map function allocates a new array rather than mutating the original. This is a core FP principle: operations return new data instead of destroying old data.

Filter — Keep Only Matching Elements

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

typedef bool (*predicate_fn)(int);

// Returns a dynamically allocated array of elements that satisfy pred.
// out_len is set to the number of surviving elements.
int *filter(const int *src, size_t len, predicate_fn pred, size_t *out_len) {
    // First pass: count matches
    size_t count = 0;
    for (size_t i = 0; i < len; i++) {
        if (pred(src[i])) count++;
    }
    *out_len = count;

    int *dest = malloc(count * sizeof(int));
    if (dest == NULL) return NULL;

    // Second pass: copy matching elements
    size_t dest_idx = 0;
    for (size_t i = 0; i < len; i++) {
        if (pred(src[i])) {
            dest[dest_idx++] = src[i];
        }
    }
    return dest;
}

bool is_even(int x) { return x % 2 == 0; }
bool is_positive(int x) { return x > 0; }

int main(void) {
    int numbers[] = {-3, 0, 4, 7, -1, 8, 2, 9};
    size_t len = sizeof(numbers) / sizeof(numbers[0]);

    size_t filtered_len;
    int *evens = filter(numbers, len, is_even, &filtered_len);
    if (evens) {
        for (size_t i = 0; i < filtered_len; i++) {
            printf("%d ", evens[i]);  // prints: 0 4 8 2
        }
        printf("\n");
        free(evens);
    }

    int *positives = filter(numbers, len, is_positive, &filtered_len);
    if (positives) {
        for (size_t i = 0; i < filtered_len; i++) {
            printf("%d ", positives[i]);  // prints: 4 7 8 2 9
        }
        printf("\n");
        free(positives);
    }
    return 0;
}

The two-pass approach—counting first, then copying—is necessary because C doesn't provide growable arrays natively. You could also use a linked list or a dynamic array library to avoid the double traversal.

Reduce / Fold — Aggregate to a Single Value

#include <stdio.h>

typedef int (*reducer_fn)(int accumulator, int element);

int reduce(const int *src, size_t len, reducer_fn fn, int initial) {
    int acc = initial;
    for (size_t i = 0; i < len; i++) {
        acc = fn(acc, src[i]);
    }
    return acc;
}

int sum(int acc, int x) { return acc + x; }
int product(int acc, int x) { return acc * x; }
int max_val(int acc, int x) { return (x > acc) ? x : acc; }

int main(void) {
    int data[] = {5, 3, 9, 1, 7};
    size_t len = sizeof(data) / sizeof(data[0]);

    int total   = reduce(data, len, sum, 0);      // 25
    int prod    = reduce(data, len, product, 1);   // 945
    int maximum = reduce(data, len, max_val, data[0]); // 9

    printf("Sum: %d, Product: %d, Max: %d\n", total, prod, maximum);
    return 0;
}

Composing map, filter, and reduce in a Pipeline

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

// Reusing the typedefs and functions from above
typedef int (*map_fn)(int);
typedef bool (*predicate_fn)(int);
typedef int (*reducer_fn)(int, int);

int *map(const int *src, size_t len, map_fn fn) { /* ... same as earlier ... */ }
int *filter(const int *src, size_t len, predicate_fn pred, size_t *out_len) { /* ... */ }
int reduce(const int *src, size_t len, reducer_fn fn, int initial) { /* ... */ }

int triple(int x) { return x * 3; }
bool greater_than_ten(int x) { return x > 10; }
int add(int a, int b) { return a + b; }

int main(void) {
    int raw[] = {1, 2, 3, 4, 5, 6};
    size_t raw_len = sizeof(raw) / sizeof(raw[0]);

    // Pipeline: map(triple) -> filter(>10) -> reduce(sum)
    int *tripled = map(raw, raw_len, triple);
    if (!tripled) return 1;

    size_t filtered_len;
    int *filtered = filter(tripled, raw_len, greater_than_ten, &filtered_len);
    free(tripled);  // intermediate result no longer needed
    if (!filtered) return 1;

    int result = reduce(filtered, filtered_len, add, 0);
    free(filtered);

    // Expected: raw = [1,2,3,4,5,6]
    // tripled = [3,6,9,12,15,18]
    // filtered (>10) = [12,15,18]
    // sum = 45
    printf("Pipeline result: %d\n", result);  // prints: 45
    return 0;
}

This pipeline pattern reads almost like a Unix shell pipeline: each stage transforms the data and hands it off to the next. The intermediate allocations must be freed explicitly—functional C requires diligent memory management.

Immutability and Pure Functions

A pure function has two properties: (1) given the same arguments, it always returns the same result; (2) it produces no side effects (no mutation of global state, no I/O, no modification of input arguments). In C, you can enforce immutability with the const qualifier and by structuring functions to return new values rather than modifying in-place.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// Pure: does not modify original, returns a new copy with appended character
char *push_char(const char *str, char ch) {
    size_t len = strlen(str);
    char *new_str = malloc(len + 2);  // +1 for ch, +1 for null terminator
    if (!new_str) return NULL;
    strcpy(new_str, str);
    new_str[len] = ch;
    new_str[len + 1] = '\0';
    return new_str;
}

// Impure version (shown for contrast) — mutates input in-place
void push_char_inplace(char *str, size_t capacity, char ch, size_t *len) {
    if (*len + 1 < capacity) {
        str[*len] = ch;
        (*len)++;
        str[*len] = '\0';
    }
}

int main(void) {
    const char *original = "hello";
    char *extended = push_char(original, '!');
    if (extended) {
        printf("Original: %s, Extended: %s\n", original, extended);
        // Original is untouched: "hello"
        // Extended is: "hello!"
        free(extended);
    }
    return 0;
}

Mark parameters const wherever possible. It communicates to the caller that the function won't modify the data, and the compiler will catch accidental mutations. For return values that shouldn't be modified downstream, consider returning const pointers (though this can complicate freeing).

Closures and State via Structs + Function Pointers

A closure is a function paired with captured environment state. In C, you can simulate closures by bundling a function pointer and its captured data into a struct. The function pointer receives a pointer to the struct as its first argument (a manual "self" or "this" pattern).

#include <stdio.h>
#include <stdlib.h>

// Closure that captures a multiplier value
typedef struct {
    int multiplier;
    int (*apply)(struct closure *, int);
} closure;

// The "method" that uses captured state
int multiply_closure(closure *c, int x) {
    return c->multiplier * x;
}

// Constructor for the closure
closure *make_multiplier(int factor) {
    closure *c = malloc(sizeof(closure));
    if (c) {
        c->multiplier = factor;
        c->apply = multiply_closure;
    }
    return c;
}

int main(void) {
    closure *times_three = make_multiplier(3);
    closure *times_seven = make_multiplier(7);

    if (times_three && times_seven) {
        // Use the closure: call through the function pointer,
        // passing the closure struct as context
        printf("3 * 10 = %d\n", times_three->apply(times_three, 10));  // 30
        printf("7 * 10 = %d\n", times_seven->apply(times_seven, 10));  // 70

        free(times_three);
        free(times_seven);
    }
    return 0;
}

For a more generic closure that can capture arbitrary data, use a void * context pointer paired with a function pointer that takes void *:

#include <stdio.h>
#include <stdlib.h>

// Generic callback: the function receives a void* context
typedef int (*callback_fn)(void *context, int input);

// A closure bundle
typedef struct {
    callback_fn fn;
    void *context;
} generic_closure;

// Example: a function that needs extra data (a threshold value)
int threshold_filter(void *context, int value) {
    int *threshold_ptr = (int *)context;
    return (value > *threshold_ptr) ? value : 0;
}

// Higher-order function that uses the generic closure
void process_array(int *arr, size_t len, generic_closure gc) {
    for (size_t i = 0; i < len; i++) {
        arr[i] = gc.fn(gc.context, arr[i]);
    }
}

int main(void) {
    int threshold = 50;
    generic_closure gc = { .fn = threshold_filter, .context = &threshold };

    int values[] = {30, 60, 45, 80, 20};
    size_t len = sizeof(values) / sizeof(values[0]);

    process_array(values, len, gc);
    // values now: [0, 60, 0, 80, 0]
    for (size_t i = 0; i < len; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");
    return 0;
}

This void * pattern is the foundation of many C libraries (like qsort from the standard library). It gives you the flexibility to pass arbitrary context through a function pointer interface.

Lazy Evaluation with Macros

Lazy evaluation defers computation until the result is actually needed. In C, you can approximate laziness with macros that expand to expressions rather than executing immediately, or by wrapping computations in thunk functions.

Macro-based Short-circuit Evaluation

#include <stdio.h>
#include <stdbool.h>

// Define a "lazy" logical OR that only evaluates the second expression
// if the first is false. Standard || already does this, but the macro
// makes the intent explicit and can wrap expensive expressions.
#define LAZY_OR(a, b) ((a) ? true : (b))

int expensive_check(void) {
    printf("Running expensive check...\n");
    return 1;  // simulate success
}

int main(void) {
    int x = 5;
    // If x > 0 is true, expensive_check() is never called
    bool result = LAZY_OR(x > 0, expensive_check());
    printf("Result: %d\n", result);  // expensive_check is NOT called
    return 0;
}

Thunk Functions for Deferred Computation

#include <stdio.h>
#include <stdlib.h>

// A thunk is a function that takes no arguments and returns a value
typedef int (*thunk_fn)(void);

// A lazy value: stores the thunk and caches the result once computed
typedef struct {
    thunk_fn thunk;
    int cached;
    bool evaluated;
} lazy_int;

int compute_expensive(void) {
    printf("Computing expensive value...\n");
    // Simulate heavy work
    return 42;
}

lazy_int *make_lazy(thunk_fn fn) {
    lazy_int *lz = malloc(sizeof(lazy_int));
    if (lz) {
        lz->thunk = fn;
        lz->cached = 0;
        lz->evaluated = false;
    }
    return lz;
}

int force(lazy_int *lz) {
    if (!lz->evaluated) {
        lz->cached = lz->thunk();
        lz->evaluated = true;
    }
    return lz->cached;
}

int main(void) {
    lazy_int *lazy_val = make_lazy(compute_expensive);
    if (!lazy_val) return 1;

    printf("Lazy value created, but not yet computed.\n");

    // First call forces evaluation
    int result1 = force(lazy_val);
    printf("First force: %d\n", result1);

    // Second call uses cached value — compute_expensive is NOT called again
    int result2 = force(lazy_val);
    printf("Second force: %d\n", result2);

    free(lazy_val);
    return 0;
}

This pattern is useful when you have expensive computations that may never be needed. The thunk captures the computation; force evaluates it on demand and memoizes the result.

Recursion and Tail-Call Optimization

Functional languages rely heavily on recursion instead of loops. In C, recursion works fine, but you must be mindful of stack depth. Modern C compilers (GCC and Clang) can perform tail-call optimization (TCO) when a recursive call is the last action in a function, effectively converting recursion into iteration at the machine level.

#include <stdio.h>

// Standard recursive factorial (not tail-recursive)
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // multiplication happens AFTER the recursive call
}

// Tail-recursive factorial — accumulator carries the result forward
int factorial_tail(int n, int acc) {
    if (n <= 1) return acc;
    return factorial_tail(n - 1, acc * n);  // recursive call is the LAST operation
}

// Wrapper with a clean public interface
int factorial_tco(int n) {
    return factorial_tail(n, 1);
}

int main(void) {
    printf("factorial(5)     = %d\n", factorial(5));       // 120
    printf("factorial_tco(5) = %d\n", factorial_tco(5));   // 120

    // The tail-recursive version can handle much larger n without stack overflow
    // if compiled with -O2 or -O3 (which enables TCO in GCC/Clang)
    printf("factorial_tco(10) = %d\n", factorial_tco(10)); // 3628800
    return 0;
}

Always structure recursive functions to be tail-recursive when possible. The accumulator pattern (passing the partial result as an extra argument) is the standard technique. Verify with your compiler's documentation that TCO is enabled at your optimization level (in GCC, -O2 or higher generally enables it).

Partial Application and Currying

Currying transforms a function that takes multiple arguments into a chain of single-argument functions. In C, you can simulate partial application by pre-binding arguments in a struct and returning a new function pointer.

#include <stdio.h>
#include <stdlib.h>

// Original: takes two arguments
int add(int a, int b) {
    return a + b;
}

// Curried version: binds first argument, returns a function taking the second
typedef int (*unary_fn)(int);

typedef struct {
    unary_fn fn;
    int bound;
} partial_app;

int add_bound(partial_app *p, int b) {
    return p->bound + b;
}

partial_app *curry_add(int a) {
    partial_app *p = malloc(sizeof(partial_app));
    if (p) {
        p->bound = a;
        // Here we would ideally return a function pointer, but since we need
        // the struct context, we wrap it. The caller uses p->fn(p, ...)
        // In practice, you'd design a cleaner closure interface.
    }
    return p;
}

// Simpler approach: manually bind with a static variable (not thread-safe!)
// or generate functions via macros.
#define MAKE_ADDER(x) \
    int add_##x(int b) { return x + b; }

// Generate specific adder functions at compile time
MAKE_ADDER(5)   // creates add_5(int b) { return 5 + b; }
MAKE_ADDER(10)  // creates add_10(int b) { return 10 + b; }

int main(void) {
    // Using the macro-generated partial applications
    printf("add_5(7)  = %d\n", add_5(7));   // 12
    printf("add_10(7) = %d\n", add_10(7));  // 17
    return 0;
}

While the macro approach lacks runtime flexibility, it demonstrates partial application at compile time—a valid technique in C for generating families of related functions.

Best Practices for Functional C

1. Separate Pure Logic from Side Effects

Structure your codebase so that pure computational functions live in one set of modules, while I/O, memory allocation, and mutation live in a thin outer layer. For example, parsing logic should be pure (taking a string, returning a parsed struct), while file reading sits in a separate function that calls the pure parser.

// Pure: no side effects, testable with any string input
typedef struct { int hour; int minute; } Time;
bool parse_time(const char *input, Time *out);  // pure

// Impure: deals with file I/O, calls parse_time internally
bool read_time_from_file(const char *filename, Time *out);  // impure wrapper

2. Use const Aggressively

Mark every pointer parameter that shouldn't be modified as const. Return const pointers when the caller shouldn't mutate the result. This transforms potential runtime bugs into compile-time errors.

// Input strings are const, output buffer is mutable
void format_name(const char *first, const char *last, char *out_buf, size_t buf_size);

// Both input and output are read-only to the caller
const int *find_median(const int *sorted_array, size_t len);

3. Prefer Returning Results Over Mutating Arguments

When a function needs to produce a modified version of data, allocate and return a new copy rather than modifying the input in-place. This preserves the original for the caller and avoids spooky action-at-a-distance bugs. If performance demands in-place mutation, document it clearly and provide both variants.

4. Use Well-Typedef'd Function Pointers

Raw function pointer syntax is hard to read and error-prone. Always create a typedef for each function signature you pass around. This also centralizes the signature definition so that changing it updates all uses.

typedef double (*math_op)(double a, double b);
typedef bool (*validator)(const char *input);
typedef void (*cleanup_fn)(void *resource);

5. Manage Memory Explicitly and Document Ownership

Functional C often allocates new memory for each transformation. Establish clear ownership rules: who allocates, who frees, and whether a function takes ownership of pointers passed to it. Consider using arena allocators or reference counting for longer-lived functional data structures to reduce allocation overhead.

6. Leverage the Standard Library's Functional Facilities

The C standard library itself contains functional gems: qsort takes a comparator function pointer; bsearch takes a comparator; atexit registers cleanup callbacks. Study these interfaces—they model the callback style that underpins functional C.

7. Use Recursion Sparingly and Test Stack Depth

Tail-call optimization is not guaranteed by the C standard; it's a compiler-specific optimization. For production code, prefer iterative loops for potentially deep recursion, or explicitly limit recursion depth and fall back to iteration. Reserve recursion for cases where the depth is provably bounded (e.g., tree traversal on balanced trees).

8. Build a Small Library of Combinators

Once you have map, filter, reduce, and a few closure utilities, collect them into a reusable header file. These abstractions pay for themselves rapidly: a five-line pipeline replaces dozens of imperative lines across your codebase.

Conclusion

Functional programming in C is not about forcing a square peg into a round hole—it's about selectively adopting FP principles that make your code safer, cleaner, and more composable while staying firmly within C's performance and portability envelope. Function pointers give you first-class functions; structs with context pointers give you closures; const gives you immutability guarantees; and macros fill in syntactic gaps. The result is a hybrid style: you write the high-level transformation logic functionally, drop into imperative loops where performance demands it, and manage memory with the same discipline you always have in C. Master these patterns, and you'll find yourself writing C that is easier to reason about, easier to test, and surprisingly elegant.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles