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 a value of one of several types, with the active alternative identified by type. When a successful operand contains acopack,&pairs its active alternative with the other operand. If both contain copacks, the result type includes every possible pairing of their alternatives;copack_for<Ts...>is a type alias tocopackthat flattens nested copacks, removes duplicates and sorts the 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. - Identity carriers with no error or empty state:
just<T>holds a value ofT, whilechoice<Ts...>is an alias forjust<copack<Ts...>>that applies monadic operations to the active alternative. - 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_r,unreachable, and thereference_constructs_from_temporaryandreference_converts_from_temporarytraits. 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. If std::type_order is unavailable, compilation fails with a diagnostic naming the requirement. The two modes use distinct ABI namespaces for fn types because they can order alternatives differently. pfn is independent of this setting. See CONTRIBUTING.md for compiler selection and instructions for building the C++26 tests and examples.
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.
The CMake targets supplied by vcpkg, Nix, FetchContent and add_subdirectory propagate the library's compiler options and C++20 requirement. Conan supplies the compiler options, but takes the C++ standard from the profile's compiler.cppstd setting. With Bazel or a plain copy of include/, configure the following explicitly:
- Select C++20 or newer:
-std=c++20with GCC or Clang, or/std:c++20with MSVC. - 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).
In Bazel, pass compiler options through --cxxopt, for example --cxxopt=-std=c++20. The shared compiler options are defined 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 CMake targets and Conan recipe 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 a 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.
With Conan, set cxx26=True and select C++26 or newer through the profile's compiler.cppstd setting. The option defines LIBFN_CXX26 on libfn::fn and makes libfn::fn_cxx26 an alias of that target.
Keep the same mode across code that exchanges fn types. 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 C++ proposals may change names or semantics as those proposals evolve. The 0.y.z numbering reflects these ongoing API changes. It does not mark releases as alpha versions or prereleases. Releases are expected to remain below 1.0.0 for the foreseeable future. Within SemVer's 0.y.z series, libfn uses the following compatibility policy:
- A change to
ymarks an API or ABI break; pin the minor version when adopting the library. - A change to
zcontains fixes or additions intended to preserve API and ABI compatibility.
Use one libfn version throughout a binary, including its dependencies, to avoid compatibility problems such as One Definition Rule (ODR) violations. Patch compatibility is an intention rather than a guarantee: the tests cannot cover every valid use of the library.
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.