Polyglot
Languages C functional
C § functional

Higher-order programming

C is not a functional language. There are no closures, no first-class environments, no immutable data structures, no algebraic data types, no pattern matching at the value level, no garbage collector. What C does have — function pointers, the comma operator, const-qualified types, static const data — is enough for a few patterns that the functional tradition rewards: callbacks, higher-order operations dispatched through function tables, configuration-by-data rather than by code. The patterns documented here are the C conventions for higher-order programming. They are limited compared to a Lisp or an ML, and they remain the conventional answer when the problem genuinely requires them.

Functions as values: function pointers

A function pointer holds the address of a function. The type of the pointer captures the function’s signature; the value can be assigned, copied, passed as an argument, returned from another function, and stored in a structure or array.

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

int (*fn)(int) = square;
int n = fn(5);             /* 25 */

The two call syntaxes — fn(5) and (*fn)(5) — are equivalent; the standard’s rules convert a function-name to a function-pointer in most contexts and a function-pointer call to a normal call when the operand is a pointer.

For full mechanics — the syntax, the typedef convention, the rules — see Pointers.

Higher-order operations through callbacks

A callback is a function pointer passed to another function so the latter can invoke it at the right moment. The standard library uses callbacks heavily for generic operations.

qsort and bsearch

The two generic algorithms in <stdlib.h> accept a comparator:

int compare_doubles(const void *a, const void *b) {
    double da = *(const double *)a;
    double db = *(const double *)b;
    return (da > db) - (da < db);
}

double values[] = {3.1, 1.4, 1.5, 9.2, 6.5};
qsort(values, 5, sizeof *values, compare_doubles);

The comparator decouples the algorithm (sorting) from the type-specific operation (comparing two elements). The decoupling is the value of higher-order programming: one routine handles all of the sorting; one routine per type handles the comparison.

A user-defined map

void int_array_map(int *arr, size_t n, int (*f)(int)) {
    for (size_t i = 0; i < n; ++i) arr[i] = f(arr[i]);
}

int double_int(int x) { return x * 2; }

int values[] = {1, 2, 3, 4, 5};
int_array_map(values, 5, double_int);

The function-pointer parameter is the operation; the array iteration is the traversal. The pattern generalises to filter, fold, and other higher-order forms; each requires a separate function because C does not have generics.

The closure problem

A closure is a function paired with its enclosing lexical environment. C functions cannot capture local variables: a function defined inside another function (which the language does not support directly) cannot reference the outer function’s locals. C’s substitute is to pass the environment explicitly, by hand, as a void * user-data argument.

The convention is that every callback type takes both an operation and a context pointer:

typedef void (*for_each_fn)(int element, void *context);

void int_array_for_each(const int *arr, size_t n, for_each_fn fn, void *context) {
    for (size_t i = 0; i < n; ++i) fn(arr[i], context);
}

void accumulate_sum(int element, void *context) {
    int *total = context;
    *total += element;
}

int sum = 0;
int values[] = {1, 2, 3, 4, 5};
int_array_for_each(values, 5, accumulate_sum, &sum);
/* sum == 15 */

The void *context is the closed-over environment, made explicit. The caller arranges for whatever data the callback needs, and the callback takes the context, casts it to its known type, and uses it.

The mechanism is verbose and unsafe: the context type is not statically checked. The conventional discipline is to define a structure for the context, document the contract at the function-pointer typedef, and never reuse a callback for a context-shape it was not written for.

The qsort comparator pattern

The qsort comparator is the prototypical higher-order interface in C. The signature:

int compare(const void *a, const void *b);

The contract:

  • Both pointers point at elements of the array qsort is processing.
  • The comparator returns a negative, zero, or positive integer as *a is less than, equal to, or greater than *b.
  • The comparator must be transitive (if a < b and b < c then a < c), anti-symmetric (if compare(a, b) > 0 then compare(b, a) < 0), and deterministic (the same inputs always produce the same result).

A common pitfall: writing return *a - *b for numeric types. The subtraction can overflow if the values straddle zero, producing the wrong sign. The portable form uses subtraction of comparison results:

int cmp_int(const void *a, const void *b) {
    int ia = *(const int *)a, ib = *(const int *)b;
    return (ia > ib) - (ia < ib);
}

The expression (ia > ib) - (ia < ib) yields -1, 0, or 1 and never overflows.

const-qualified types and immutability

C has no first-class immutable types. The closest mechanism is const, which marks an lvalue as not modifiable through itself:

const int  zero = 0;          /* zero may not be modified through this name */
const char *greeting = "hello"; /* the chars may not be modified through greeting */

const is shallow and per-name: another non-const reference to the same object can modify it, and the standard does not require the implementation to detect or prevent the inconsistency. The principal value of const is documentation and limited compile-time checking, not genuine immutability.

The conventional uses:

  • Function parameters that the function does not modify: void f(const widget *w).
  • File-scope constants: static const double pi = 3.14159265358979323846.
  • Read-only string literals: const char *banner = "polyglot".

static const data — read-only tables, configuration arrays — is one of the few mechanisms by which a C program can be data-driven rather than code-driven, and is the conventional analogue of “configuration as values” in functional traditions.

Configuration as data

Where a higher-level language might define behaviour by constructing functions or partial applications, C conventionally defines behaviour by data: a structure of values that drives a generic algorithm.

typedef struct {
    const char *option;
    int         (*handler)(const char *value);
    const char *help;
} cli_option;

static int handle_verbose(const char *v)  { (void)v; verbose = 1; return 0; }
static int handle_output(const char *v)   { strncpy(output_path, v, sizeof output_path - 1); return 0; }

static const cli_option options[] = {
    { "--verbose", handle_verbose, "enable verbose logging" },
    { "--output",  handle_output,  "set output path" },
    { NULL, NULL, NULL },
};

The dispatch loop iterates the table:

for (const cli_option *o = options; o->option; ++o) {
    if (strcmp(arg, o->option) == 0) {
        return o->handler(value);
    }
}

The data table is the value-level encoding of what an object-oriented or functional language would express directly. The table can be extended without changing the dispatch logic; the handler functions are independent of one another and of the iteration code. The pattern is C’s conventional answer to plug-in architectures, command dispatch, parser tables, and similar problems whose extension axis is “more behaviours” rather than “more state”.

Currying and partial application

C does not support currying or partial application. The closest equivalent is to define a structure carrying the bound arguments:

typedef struct {
    int (*operation)(int, int);
    int  bound;
} bound_operation;

int apply(bound_operation *op, int x) {
    return op->operation(op->bound, x);
}

int add(int a, int b) { return a + b; }

bound_operation add_5 = { add, 5 };
int result = apply(&add_5, 10);   /* 15 */

The construction is awkward and rarely used in idiomatic C. A function written with no curried form — add(5, 10) directly — is conventionally clearer. Where partial application is genuinely needed (in a callback that needs context), the explicit context-pointer pattern above is the standard answer.

Pure functions

C has no notion of purity in the language: functions may freely modify global state, read clocks, perform I/O. Some functions can be marked with implementation-specific attributes (GCC’s __attribute__((pure)) or __attribute__((const))) that hint to the optimiser that the function has no side effects, but the language standard does not mandate or detect purity.

The conventional discipline:

  • Functions that compute a value from their arguments, with no global reads or writes, are easier to reason about and easier to test. They are the prevailing design in numerical code, parsing, and small data-transformation utilities.
  • Functions that read or write global state should signal it in their name or signature: next_id() rather than get_next_id(), or by making the state explicit through a context parameter.

C’s lack of language-level purity does not mean pure-style design is impossible; it means the discipline is a stylistic choice rather than a language feature.

A note on what is and is not available

The features that the functional tradition relies on, and their status in C:

FeatureAvailable in C?
First-class functionsAs function pointers, yes.
ClosuresNo. The void *context pattern is the substitute.
Anonymous functions / lambdasNo. Named static functions are the substitute.
Higher-order functions (map, filter, fold)Possible per-type; not generic across types.
Currying / partial applicationNot directly; emulated via bound-argument structures.
Immutable dataLimited via const; not a separate type system.
Algebraic data typesNo; tagged unions are the substitute.
Pattern matchingNo; switch and field destructuring are the substitute.
Tail-call optimisationImplementation-defined; not guaranteed.
Persistent data structuresNo standard-library support; third-party only.

The picture is consistent: C admits functional patterns where the cost is moderate (function pointers, callbacks, data-driven dispatch) and resists them where the cost is high (closures, ADTs, persistence). Programs that genuinely require the resisted features are typically written in languages whose runtimes support them, or in C against a library (such as a Lisp embedded in C) that supplies the missing mechanism.