Functional programming in C++
libfn extends C++ vocabulary types such as expected and optional and adds monadic carrier types choice and just. It provides monadic combinators such as and_then, transform and or_else, along with other functional facilities. Its purpose is to develop and test functional programming patterns for future C++ standardization.
This example parses rational numbers and applies arithmetic operations. Each operation returns an expected whose error type describes its possible failures. The complete example includes the parser and required headers.
enum class NotANumber {};
enum class DivByZero {};
enum class Overflow {};
enum class Add {};
enum class Sub {};
enum class Mul {};
enum class Div {};
// Parse a numerator and optional denominator; without '/', the denominator is 1.
constexpr auto parse(std::string_view s) noexcept
-> fn::expected<fn::pack<int, int>, fn::copack<NotANumber>>;
class Rational {
int n_, d_;
constexpr Rational(int n, int d) noexcept : n_(n), d_(d) {}
public:
constexpr auto operator==(Rational const &) const noexcept -> bool = default;
constexpr auto num() const noexcept -> int { return n_; }
constexpr auto den() const noexcept -> int { return d_; }
// Construct a reduced fraction with a positive denominator and both terms representable as int.
static constexpr struct make_t {
constexpr auto operator()(long long n, long long d) const noexcept
-> fn::expected<Rational, fn::copack_for<DivByZero, Overflow>>
{
if (d == 0) return fn::unexpected{fn::copack{DivByZero{}}};
// std::gcd requires both |n| and |d| to be representable as long long.
if (n == std::numeric_limits<long long>::min() || d == std::numeric_limits<long long>::min())
return fn::unexpected{fn::copack{Overflow{}}};
auto const g = (d < 0 ? -1 : 1) * std::gcd(n, d);
n /= g;
d /= g;
if (n < std::numeric_limits<int>::min() || n > std::numeric_limits<int>::max()
|| d > std::numeric_limits<int>::max()) {
return fn::unexpected{fn::copack{Overflow{}}};
}
return Rational(static_cast<int>(n), static_cast<int>(d));
}
constexpr auto operator()(std::string_view s) const noexcept -> decltype(auto)
{
return parse(s) | fn::and_then(*this);
}
} make{};
constexpr auto neg() const noexcept -> decltype(auto) { return make(-1LL * n_, d_); }
constexpr auto inv() const noexcept -> decltype(auto) { return make(d_, n_); }
constexpr auto add(Rational const &other) const noexcept -> decltype(auto)
{
return make(1LL * n_ * other.d_ + 1LL * other.n_ * d_, //
1LL * d_ * other.d_);
}
constexpr auto sub(Rational const &other) const noexcept -> decltype(auto)
{
return other.neg() | fn::and_then([*this](Rational y) { return add(y); });
}
constexpr auto mul(Rational const &other) const noexcept -> decltype(auto)
{
return make(1LL * n_ * other.n_, 1LL * d_ * other.d_);
}
constexpr auto div(Rational const &other) const noexcept -> decltype(auto)
{
return other.inv() | fn::and_then([*this](Rational y) { return mul(y); });
}
};
// Combine parsing and arithmetic errors in the deduced result type.
constexpr auto evaluate(std::string_view a, fn::copack_for<Add, Sub, Mul, Div> op,
std::string_view b) noexcept -> decltype(auto)
{
auto const operation = fn::expected_unit{}.transform([op] { return op; });
return (Rational::make(a) & operation & Rational::make(b)) //
| fn::and_then(fn::overload{[](Rational x, Add, Rational y) { return x.add(y); },
[](Rational x, Sub, Rational y) { return x.sub(y); },
[](Rational x, Mul, Rational y) { return x.mul(y); },
[](Rational x, Div, Rational y) { return x.div(y); }});
}
// Both results include every error type from their stages.
static_assert(
std::is_same_v<decltype(Rational::make("1/1")),
fn::expected<Rational, fn::copack_for<DivByZero, NotANumber, Overflow>>>);
static_assert(
std::is_same_v<decltype(evaluate("1/2", Add{}, "3/4")),
fn::expected<Rational, fn::copack_for<DivByZero, NotANumber, Overflow>>>);
// Check a successful calculation and division by zero at compile time.
static_assert(evaluate("1/2", Add{}, "1/3").value() == Rational::make(5, 6));
static_assert(evaluate("2/3", Div{}, "0/1").error().has_value<DivByZero>());The example combines several library features:
- Monadic sequences —
operator|connects a result to a combinator such asfn::and_then(f), which callsfon success and propagates errors otherwise. - Graded errors — parsing and arithmetic can fail in different ways. The library combines their error types into a
copack; here, the result usescopack_for<DivByZero, NotANumber, Overflow>. Callers do not need to combine the error types by hand. - Composing values —
operator&combines successful operands into apack, preserving their order. Apackholds values of different types and passes them as separate arguments to the next call. For example,parsereturns apack<int, int>thatand_thenpasses to the two-argument overload ofmake. - Composing alternatives — a
copackholds one of several types, identified by type rather than by position (as instd::variant). When a value side is acopack,&pairs each alternative with the other operand. Two copacks produce all combinations of alternatives; the library flattens, deduplicates and sorts the resulting alternative types. - Multidispatch —
fn::overloadselects an arithmetic operation for the active alternative ofop. Each handler receives the operands as separate arguments. Every possible alternative needs a matching handler, or compilation fails. - Identity carrier —
expected<T, copack<>>cannot hold a failure state. The example starts withfn::expected_unit, an alias forexpected<void, copack<>>, and uses its.transformmember to supplyopas the value without adding a failure mode.
make is a smart constructor: it returns either a valid Rational or an error. A successful result is reduced, has a positive denominator, and fits in two int values. Because make is a callable object, and_then can accept it with both overloads intact.
The library also provides:
- Combinators for transforming, inspecting and recovering results, including
transform,inspect,or_elseandrecover. - Pipelines over
optional. - The
choiceidentity carrier for applying monadic operations to acopack. - The
justidentity carrier. - Simultaneous disjunction:
operator|selects a successful operand or combines the errors. fn::disjoinandfn::conjoinfold disjunction and conjunction over multiple operands.- Tuple access for
pack, includingget<I>(p)and structured bindings. - Structural
packandcopacktypes: when their element types meet the requirements, their values can be template arguments. - Support for immovable values and callables, and for user-defined combinators.
libfn performs no I/O and makes no dynamic allocations of its own. Its only explicit exception path is value() on a result without a value (as required by the C++ standard). Contained types and callables may allocate or throw. libfn does not leak resources it manages when user code throws. Selected operations, including copack assignment, provide the strong exception guarantee: if the operation throws, the destination retains its previous value. Operations support constant evaluation when their inputs and callables allow it, as the example's static_asserts demonstrate.
See examples/ and the API reference for more. TYPE_ALGEBRA.md explains the products and sums behind pack and copack, how the monadic combinators compose, and the laws exercised by the tests.
The library has two layers:
pfn(include/pfn, namespacepfn) provides C++20 polyfills for standard-library facilities through C++26:expected,optional(including monadic operations,optional<T&>and range support),invoke_randunreachable. These follow the C++ standard and accepted proposals, includinghas_error().fn(include/fn, namespacefn) builds on those polyfills. It adds combinators such asand_then,transformandrecover, along with the vocabulary typespack,copackandchoice.
The fn types extend their pfn counterparts: switching a valid program from pfn types to fn changes neither compilation nor program behaviour, while making the types and operations defined in fn available.
The default mode requires C++20. Supported compilers are GCC 12 or later, Clang 19 or later, Apple Clang 21.0 or later, and MSVC from Visual Studio 2022 or later. For older toolchains, use the 0.1.0 release. CONTRIBUTING.md explains how to set up a supported toolchain.
The library needs a total ordering of types to normalize copack alternatives. Its default implementation does not support unnamed types or types without linkage, such as local types and lambdas. The ordering can also differ between GCC and Clang.
The opt-in LIBFN_CXX26 mode uses C++26's std::type_order and requires a compiler that implements it, such as GCC 16. Because the modes can order alternatives differently, each gives fn types a distinct ABI namespace. pfn is independent of this setting. See CONTRIBUTING.md for the mode's requirements.
libfn is available in the Bazel Central Registry and vcpkg registry. This repository also provides packaging for Conan, vcpkg, Nix and Bazel, all exercised by CI. You can also use CMake's FetchContent or add_subdirectory.
Every packaging route above except Bazel propagates the library's compile options. With Bazel or a plain copy of include/, set them explicitly:
- Select C++20 or newer:
-std=c++20with GCC or Clang, or/std:c++20with MSVC. In Bazel, pass compiler options through--cxxopt, for example--cxxopt=-std=c++20. - With Clang and Apple Clang, use
-Wno-missing-bracesto suppress warnings about the intentional brace elision infn::packinitialization. - With MSVC, use
/permissive-and/D_HAS_CXX23=1(see the compatibility note below).
The authoritative set is the INTERFACE options in cmake/CompilationOptions.cmake.
In MSVC's C++20 mode, <exception> includes <eh.h>, which declares a global function named unexpected. With using namespace pfn, an unqualified use of unexpected is ambiguous, so the following code fails to compile:
#include <pfn/expected.hpp>
using namespace pfn;
int main() {
return unexpected(20) == unexpected(19);
}To suppress the legacy declaration, use one of these options:
- Select
/std:c++latestor, where supported,/std:c++23preview. With CMake, use-DCMAKE_CXX_STANDARD=23to select C++23 mode. - To keep C++20 mode, define
_HAS_CXX23=1for the project. The exported CMake targets provide this definition automatically.
To install from a source checkout or an unpacked release tarball, run from the project directory:
cmake -B .build -DLIBFN_TESTS=OFF
cmake --install .build-DLIBFN_TESTS=OFF avoids fetching test dependencies. The header-only package needs no build step. On Linux and macOS, the default install prefix is /usr/local, so installation may need sudo.
To install under your home directory, choose the custom prefix at install time:
cmake --install .build --prefix "$HOME/.local"Or set CMAKE_INSTALL_PREFIX when configuring:
cmake -B .build -DLIBFN_TESTS=OFF -DCMAKE_INSTALL_PREFIX="$HOME/.local"
cmake --install .buildThe installed package supports find_package(libfn CONFIG REQUIRED). For a custom prefix, pass -DCMAKE_PREFIX_PATH="$HOME/.local" (or your chosen prefix) when configuring the consuming project.
The CMake package exports libfn::fn and libfn::pfn:
find_package(libfn CONFIG REQUIRED)
target_link_libraries(main PRIVATE libfn::fn) # or libfn::pfn for the polyfills aloneFor LIBFN_CXX26 mode, use libfn::fn_cxx26 in place of libfn::fn. It supplies the same headers, defines LIBFN_CXX26, and selects C++26. A consumer should link exactly one of these two targets. libfn::pfn has no separate C++26 target.
If the compiler lacks std::type_order, a header diagnostic names the missing feature. Keep the same mode across code that exchanges fn types: their ABI namespaces differ. For example, a function declared with an fn parameter in one mode will not link to a definition using the other mode.
A single-header distribution contains the entire library in one file for online compilers and standalone reproducers where include paths cannot be configured.
For regular projects, prefer the separate headers: they give more useful file paths in diagnostics.
Choose a download according to whether you need a fixed version:
- Versioned URL:
https://libfn.org/v<x.y.z>/libfn.hpp(all versions). Each release keeps its own copy. Compiler Explorer can include it directly by URL. - Release attachment:
libfn-v<x.y.z>.hppon each GitHub release. Verify its build provenance withgh attestation verify libfn-v<x.y.z>.hpp --repo libfn/functional. - Latest release:
https://libfn.org/libfn.hpp. This URL changes with each release; use it for experiments, and pin a version when you need reproducible builds.
Replace <x.y.z> with a released version, such as 0.1.0. The compile options also apply to the single header. To select C++26 mode, define LIBFN_CXX26 and compile as C++26.
Facilities in include/fn that track a C++ proposal may change names or semantics as the proposal evolves. Breaking changes increase the minor version (y in 0.y.z), so pin that version when adopting the library.
Releases numbered 0.y.z (including 0.1.0) are mature releases, not alpha versions or prereleases. Releases are expected to remain below 1.0.0 for the foreseeable future because C++ standardization may continue to reshape the API. Within SemVer’s 0.y.z series, libfn uses the following compatibility policy:
- A change to
ymarks an API or ABI break. - A change to
zcontains fixes or additions and is expected to preserve compatibility.
Use a single libfn version per binary to minimize compatibility risks, including One Definition Rule (ODR) violations. Patch releases are intended, but not guaranteed, to preserve API and ABI compatibility. We cannot test every valid use of the library, so users remain responsible for ensuring that their dependencies use a consistent version.
See CONTRIBUTING.md for development setup, builds, tests, version updates and pre-commit checks. CHANGELOG.md records the design decisions and release history.
- Gašper Ažman, whose "(Fun)ctional C++ and the M-word" inspired this library.
- Bartosz Milewski, for explaining parametrised and graded monads and effect systems.
- Mykola Golubyev, for fixes to znai that this project needed.
- Ripple, for giving the main author time to work on the library.
Distributed under the ISC License; see LICENSE.md for the terms.