Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Design history of libfn, newest first. The living documents — [README.md](README.md), [CONTRIBUTING.md](CONTRIBUTING.md), [docs/](docs/) — describe only the present state of the design; when a decision makes an earlier idea obsolete, this file is where the transition is recorded and explained.

## `pack` and `copack` operations move to `<fn/algebra.hpp>` — 27 September 2026

`operator&` over `pack` and `copack`, `conjoin`, and `disjoin` move to `<fn/algebra.hpp>`. Include this header when using these operations without a carrier header. The `expected`, `optional`, and `just` headers include it.

`<fn/copack.hpp>` includes `<fn/pack.hpp>`; the reverse dependency is removed. A copack transform returning `void` therefore needs only `<fn/copack.hpp>`. Code using `copack` through `<fn/pack.hpp>` must include `<fn/copack.hpp>` explicitly.

## `&` takes a `void` side as the unit factor `pack<>` — 27 September 2026

`&` treats a `void` value as the unit factor `pack<>`. For example, `expected<int, E> & just<void>` yields `expected<pack<int>, E>`; previously it yielded `expected<int, E>`. This breaking change applies across carrier pairings in either order. Copack values distribute into packs, and `optional<T&> & just<void>` yields an owning `optional<pack<T>>`. Two `void` values still yield `void`.
Expand Down
40 changes: 20 additions & 20 deletions docs/reference/conjoin.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "fold fn::conjoin"
---

##### Defined in {style: "api", badge: "#include <fn/pack.hpp>"}
##### Defined in {style: "api", badge: "#include <fn/algebra.hpp>"}

---

Expand All @@ -29,54 +29,54 @@ conjoin_t conjoin; // (1)

## The operator {style: "api"}

The binary conjunction each carrier declares; `conjoin` is its n-ary fold.
Binary conjunction of data or carriers; `conjoin` is its n-ary fold.

```cpp {title: "fn::operator&"}
constexpr auto operator&(auto &&lh, auto &&rh); // (1)

template <typename Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (1)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (2)

template <typename Lh, some_expected Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (2)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (3)

template <some_expected Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (3)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (4)

template <typename Lh, some_expected_void Rh>
constexpr auto operator&(Lh &&, Rh &&rh); // (4)
constexpr auto operator&(Lh &&, Rh &&rh); // (5)

template <some_expected_void Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&); // (5)
constexpr auto operator&(Lh &&lh, Rh &&); // (6)

template <typename Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (6)
constexpr auto operator&(Lh &&, Rh &&) -> just<void>; // (7)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (7)
constexpr auto operator&(Lh &&, Rh &&) -> just<void>; // (8)

template <some_optional Lh, some_optional Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (8)

template <typename Lh, some_optional Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (9)

template <some_optional Lh, typename Rh>
template <typename Lh, some_optional Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (10)

template <typename Lh, some_optional Rh>
template <some_optional Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (11)

template <some_optional Lh, typename Rh>
template <typename Lh, some_optional Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (12)

constexpr auto operator&(auto &&lh, auto &&rh); // (13)
template <some_optional Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (13)
```

:include-doxygen-doc: fn::operator& { args: "Lh &&, Rh &&" }

:include-doxygen-doc-params: fn::operator& { args: "Lh &&, Rh &&", title: "parameters" }

:include-doxygen-doc: fn::operator& { args: "auto &&, auto &&" }

:include-doxygen-doc-params: fn::operator& { args: "auto &&, auto &&", title: "parameters" }

:include-doxygen-doc: fn::operator& { args: "Lh &&, Rh &&" }

:include-doxygen-doc-params: fn::operator& { args: "Lh &&, Rh &&", title: "parameters" }

---

## Call signatures {style: "api"}
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/disjoin.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "fold fn::disjoin"
---

##### Defined in {style: "api", badge: "#include <fn/pack.hpp>"}
##### Defined in {style: "api", badge: "#include <fn/algebra.hpp>"}

---

Expand Down
2 changes: 2 additions & 0 deletions docs/reference/pack.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ template <std::size_t I, some_pack P>
constexpr auto get(P &&p) -> decltype(auto); // (3)
```

Overloads (1) and (2) take a `copack` and are defined in `<fn/copack.hpp>`.

:include-doxygen-doc: fn::get { args: "Cp &&" }

:include-doxygen-doc-params: fn::get { args: "Cp &&", title: "parameters" }
Expand Down
1 change: 1 addition & 0 deletions examples/calculator/calculator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef EXAMPLES_CALCULATOR_CALCULATOR
#define EXAMPLES_CALCULATOR_CALCULATOR

#include <fn/algebra.hpp>
#include <fn/and_then.hpp>
#include <fn/expected.hpp>
#include <fn/pack.hpp>
Expand Down
1 change: 1 addition & 0 deletions examples/type_algebra/main.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <fn/algebra.hpp>
#include <fn/and_then.hpp>
#include <fn/concepts.hpp>
#include <fn/copack.hpp>
Expand Down
1 change: 1 addition & 0 deletions include/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ set(INCLUDE_FN_HEADERS
fn/detail/pack_impl.hpp
fn/detail/traits.hpp
fn/detail/variadic_union.hpp
fn/algebra.hpp
fn/and_then.hpp
fn/concepts.hpp
fn/copack.hpp
Expand Down
240 changes: 240 additions & 0 deletions include/fn/algebra.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
// Copyright (c) 2026 Bronek Kozicki
//
// Distributed under the ISC License. See accompanying file LICENSE.md
// or copy at https://opensource.org/licenses/ISC

#ifndef INCLUDE_FN_ALGEBRA
#define INCLUDE_FN_ALGEBRA

#include <fn/copack.hpp>
#include <fn/detail/functional.hpp>
#include <fn/detail/traits.hpp>
#include <fn/monadic.hpp>
#include <fn/pack.hpp>
#include <libfn_version.hpp>

#include <type_traits>
#include <utility>

#include <fn/detail/macro_begin.hpp>

namespace fn {
inline namespace LIBFN_VERSION {

namespace detail {

// The join checks has_value() before value(); exclude the accessor
// from noexcept calculations for folding and result construction.
template <typename Monad> using _value_of_t = decltype(::std::declval<Monad>().value());

template <typename Monad>
using _factor_t = ::std::conditional_t<::std::is_void_v<typename ::std::remove_cvref_t<Monad>::value_type>,
::fn::pack<>, typename ::std::remove_cvref_t<Monad>::value_type>;
template <typename Monad>
using _factor_of_t = ::std::conditional_t<::std::is_void_v<typename ::std::remove_cvref_t<Monad>::value_type>,
::fn::pack<>, _value_of_t<Monad>>;

template <typename Monad> [[nodiscard]] constexpr auto _factor(Monad &&m) -> _factor_of_t<Monad>
{
if constexpr (::std::is_void_v<typename ::std::remove_cvref_t<Monad>::value_type>)
return {};
else
return FWD(m).value();
}

// An uninhabited factor has no value fold. Avoid instantiating it in types,
// noexcept specifications and bodies.
template <typename Lh, typename Rh>
constexpr inline bool _uninhabited_join = empty_copack<typename ::std::remove_cvref_t<Lh>::value_type>
|| empty_copack<typename ::std::remove_cvref_t<Rh>::value_type>;

template <bool Uninhabited, typename Lh, typename Rh> struct _joined {
using type = copack<>;
};
template <typename Lh, typename Rh> struct _joined<false, Lh, Rh> {
using type = decltype(::fn::detail::_fold_detail::fold<_factor_t<Lh>, _factor_t<Rh>>(
::std::declval<_factor_of_t<Lh>>(), ::std::declval<_factor_of_t<Rh>>()));
};
template <typename Lh, typename Rh> using _joined_t = typename _joined<_uninhabited_join<Lh, Rh>, Lh, Rh>::type;

// Match the named efn parameter, which _join invokes as an lvalue.
template <bool Uninhabited, template <typename> typename Tpl, typename Lh, typename Rh, typename Efn>
struct _nothrow_join_arm {
static constexpr bool value = ::std::is_nothrow_invocable_v<Efn &, Lh> && ::std::is_nothrow_invocable_v<Efn &, Rh>
&& _nothrow_initializable<Tpl<copack<>>, ::std::invoke_result_t<Efn &, Lh>>
&& _nothrow_initializable<Tpl<copack<>>, ::std::invoke_result_t<Efn &, Rh>>;
};
template <template <typename> typename Tpl, typename Lh, typename Rh, typename Efn>
struct _nothrow_join_arm<false, Tpl, Lh, Rh, Efn> {
static constexpr bool value = noexcept(::fn::detail::_fold_detail::fold<_factor_t<Lh>, _factor_t<Rh>>(
::std::declval<_factor_of_t<Lh>>(), ::std::declval<_factor_of_t<Rh>>()))
&& _nothrow_initializable<Tpl<_joined_t<Lh, Rh>>, ::std::in_place_t, _joined_t<Lh, Rh>>
&& ::std::is_nothrow_invocable_v<Efn &, Lh> && ::std::is_nothrow_invocable_v<Efn &, Rh>
&& _nothrow_initializable<Tpl<_joined_t<Lh, Rh>>, ::std::invoke_result_t<Efn &, Lh>>
&& _nothrow_initializable<Tpl<_joined_t<Lh, Rh>>, ::std::invoke_result_t<Efn &, Rh>>;
};
template <template <typename> typename Tpl, typename Lh, typename Rh, typename Efn>
constexpr inline bool _nothrow_join = _nothrow_join_arm<_uninhabited_join<Lh, Rh>, Tpl, Lh, Rh, Efn>::value;

template <typename Lh, typename Rh>
using _disjoined_t = copack_for<_sum_element_t<typename ::std::remove_cvref_t<Lh>::value_type>,
_sum_element_t<typename ::std::remove_cvref_t<Rh>::value_type>>;

template <typename T> constexpr inline bool _dead_value = empty_copack<typename ::std::remove_cvref_t<T>::value_type>;

// Uninhabited values cannot reach the injection arm.
template <bool Dead, typename Type, typename Side> struct _nothrow_disj_inject {
static constexpr bool value = true;
};
template <typename Type, typename Side> struct _nothrow_disj_inject<false, Type, Side> {
static constexpr bool value
= _nothrow_initializable<Type, ::std::in_place_t, decltype(::std::declval<Side>().value())>;
};

template <template <typename> typename Tpl>
[[nodiscard]] constexpr auto _join(auto &&lh, auto &&rh, auto &&efn) //
noexcept(_nothrow_join<Tpl, decltype(lh), decltype(rh), decltype(efn)>)
-> Tpl<_joined_t<decltype(lh), decltype(rh)>>
{
using type = Tpl<_joined_t<decltype(lh), decltype(rh)>>;
if constexpr (_uninhabited_join<decltype(lh), decltype(rh)>) {
if (not lh.has_value())
return type{efn(FWD(lh))};
else
return type{efn(FWD(rh))};
} else {
using Lh = _factor_t<decltype(lh)>;
using Rh = _factor_t<decltype(rh)>;
if (lh.has_value() && rh.has_value())
return type{::std::in_place, ::fn::detail::_fold_detail::fold<Lh, Rh>(_factor(FWD(lh)), _factor(FWD(rh)))};
else if (not lh.has_value())
return type{efn(FWD(lh))};
else
return type{efn(FWD(rh))};
}
}

} // namespace detail

/**
* @brief The data conjunction: concatenates into a `pack`, distributing over `copack` alternatives
*
* With plain data on both sides the fields concatenate into one flat `pack`. When either operand
* is a `copack`, the product distributes over its alternatives - two copacks yield the full
* cartesian product - producing a normalized `copack` of `pack`s. Dispatches on its left operand:
* a bare `scalar & scalar` is not part of the algebra, so lift one side first, as in
* `fn::as_pack(a) & b`.
*
* @param lh A `pack` or a `copack`
* @param rh The data to conjoin: a scalar, a `pack` or a `copack`
* @return A `pack`, or a `copack` of `pack`s where alternatives distribute
*/
[[nodiscard]] constexpr auto operator&(auto &&lh, auto &&rh) //
noexcept(noexcept(::fn::detail::_fold_detail::fold<::std::remove_cvref_t<decltype(lh)>,
::std::remove_cvref_t<decltype(rh)>>(FWD(lh), FWD(rh))))
requires(some_copack<decltype(lh)> || some_pack<decltype(lh)>)
{
using Lh = ::std::remove_cvref_t<decltype(lh)>;
using Rh = ::std::remove_cvref_t<decltype(rh)>;
return ::fn::detail::_fold_detail::fold<Lh, Rh>(FWD(lh), FWD(rh));
}

namespace detail {
// Reject carriers here to avoid silently storing them as pack elements.
template <typename... Ts>
concept _no_carrier = (... && (not some_monadic_type<Ts>));

template <typename... Ts>
concept _all_carriers = (... && some_monadic_type<Ts>);
} // namespace detail

/**
* @brief The n-ary fold of `operator &` above; a single argument is forwarded unchanged
*
* Arguments must be all carriers or all data. Data forms a product, with a leading scalar
* lifted into a `pack`; carriers compose through their `operator &`.
*/
constexpr inline struct conjoin_t {
/**
* @brief Forwards a single argument unchanged
* @param arg The argument
* @return The argument, forwarded
*/
template <typename Arg> [[nodiscard]] constexpr auto operator()(Arg &&arg) const -> decltype(arg) { return FWD(arg); }

/**
* @brief Folds data into a product, or carriers into their conjunction
*
* @param arg The leading argument
* @param args Further arguments - all data, or all carriers, never the two mixed
* @return The folded product, or the folded conjunction
*/
template <typename Arg, typename... Args>
requires(not some_copack<Arg>) && (not some_pack<Arg>) && detail::_no_carrier<Arg, Args...>
[[nodiscard]] constexpr auto operator()(Arg &&arg, Args &&...args) const
{
return (::fn::pack{FWD(arg)} & ... & FWD(args));
}

template <typename Arg, typename... Args>
requires(some_copack<Arg> || some_pack<Arg>) && detail::_no_carrier<Args...>
[[nodiscard]] constexpr auto operator()(Arg &&arg, Args &&...args) const
{
return (FWD(arg) & ... & FWD(args));
}

template <typename Arg, typename... Args>
requires(sizeof...(Args) > 0)
&& detail::_all_carriers<Arg, Args...> && requires(Arg &&a, Args &&...as) { (FWD(a) & ... & FWD(as)); }
[[nodiscard]] constexpr auto operator()(Arg &&arg, Args &&...args) const //
noexcept(noexcept((FWD(arg) & ... & FWD(args))))
{
return (FWD(arg) & ... & FWD(args));
}
} conjoin; ///< The n-ary conjunction: `conjoin(a, b, c)`

/**
* @brief The n-ary fold of the disjunction `operator |` over the monadic carriers; a single
* argument is forwarded unchanged
*
* Carriers only, in every arity - disjunction has no data-level form. An identity-cluster operand
* makes the whole disjunction total, folding the result into `just` or `choice`.
*/
constexpr inline struct disjoin_t {
/**
* @brief Forwards a single carrier unchanged
* @param arg The carrier
* @return The carrier, forwarded
*/
template <some_monadic_type Arg> [[nodiscard]] constexpr auto operator()(Arg &&arg) const -> decltype(arg)
{
return FWD(arg);
}

/**
* @brief Folds the carriers into their disjunction
*
* The n-ary form of `operator |`: the result holds the first operand that worked, its values
* summing into a `copack`, and the errors multiply into a `pack` reached only where every
* operand failed. An identity-cluster operand makes the whole disjunction total.
*
* @param arg The leading carrier
* @param args Further carriers to disjoin
* @return The folded disjunction
*/
template <typename Arg, typename... Args>
requires(sizeof...(Args) > 0)
&& detail::_all_carriers<Arg, Args...> && requires(Arg &&a, Args &&...as) { (FWD(a) | ... | FWD(as)); }
[[nodiscard]] constexpr auto operator()(Arg &&arg, Args &&...args) const //
noexcept(noexcept((FWD(arg) | ... | FWD(args))))
{
return (FWD(arg) | ... | FWD(args));
}
} disjoin; ///< The n-ary disjunction: `disjoin(a, b, c)`

} // namespace LIBFN_VERSION
} // namespace fn

#include <fn/detail/macro_end.hpp>

#endif // INCLUDE_FN_ALGEBRA
Loading