Skip to content
Open
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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.

## `optional<T&>` refuses pack and copack referents — 3 October 2026

`fn::optional<T&>` no longer accepts a `pack` or `copack` referent, matching `just<T&>`. `fn::optional<copack<Ts...>&>` dispatched on its referent's active alternative, and `fn::optional<pack<Ts...>&>` expanded its referent's fields. How a reference to a product or a sum takes part in conjunction, disjunction and grading is an open question, so both are refused until the type algebra answers it; issue #434 discusses the copack case. `pfn::optional<T&>` does not reject them, but `fn` and `pfn` types are not meant to be used together.

## `<fn/monadic.hpp>` becomes `<fn/traits.hpp>`; `monadic_invocable` rejects an incomplete functor — 27 September 2026

Replace includes of `<fn/monadic.hpp>` with `<fn/traits.hpp>`; no compatibility header is provided. `some_in_place_type` moves to `<fn/traits.hpp>` and remains available through `<fn/copack.hpp>`.
Expand Down Expand Up @@ -44,6 +48,21 @@ Results are converted before spliced arguments expire, fixing a dangling referen

`void` lifts to `copack<pack<>>`, including in `same_value_kind` and pipeline recovery into `optional`. A recovery returned by reference from `optional` has its copy included in the `noexcept` specification.

## `just` holds lvalue references — 24 September 2026

`just<T&>` now supports lvalue-reference payloads (issue #417), providing an always-engaged counterpart to `optional<T&>`. Referents must be object types other than arrays, in-place type tags, packs, or copacks. Rvalue-reference payloads remain unsupported, as in `optional` and `pack`.

`just<T&>` follows `optional<T&>`: copy assignment and `emplace` rebind, comparisons compare the referents, and `value_type` is `T`. `value()` returns `T&` regardless of the carrier's value category or constness. Callable operations use that same reference, expanding tuple-like referents as `fn::apply` does. `apply_type` tags the payload with `std::in_place_type<T&>`.

There is no default constructor. For an `int x`, `just{x}` still deduces `just<int>`; the explicit type tag in `just(std::in_place_type<int&>, x)` instead deduces `just<int&>`. As with the library's `optional<T&>`, binding to a temporary is not yet rejected and can leave a dangling reference.

Composition follows these rules:

- A `transform` callback returning a supported lvalue reference `U&` produces `just<U&>`, referring to the returned object. An rvalue owning `just` rejects such a result: it may refer into the payload, which expires with the carrier. `optional<T>` accepts it, as the standard specifies; `just` deliberately diverges.
- Member and pipeline `and_then` accept callbacks returning `just<U&>`. When every branch of a choice returns the same `just<U&>` type, the result retains that type. A join of different result types is rejected if any is a reference `just`, because references cannot be alternatives of the resulting choice. This matches the restriction on reference payloads in optional joins.
- The products and sums that `&` and `|` build hold a copy of the referent, as they do for `optional<T&>`. This includes a product with the unit `just<void>`: `just<void>{} & just<T&>{x}` is `just<pack<T>>`.
- Copack references remain unsupported. `just<copack<Ts...>&>` would dispatch on the referent's active alternative, introducing control flow based on state the carrier does not own. Issue #434 discusses the tradeoffs and open questions. `transform` also rejects copack-reference results.

## Singular copacks support the tuple protocol — 24 September 2026

A `copack` with exactly one alternative supports the tuple protocol: `std::tuple_size_v<copack<T>>` is 1, `std::tuple_element_t<0, copack<T>>` is `T`, and `get<0>` returns the same reference as the index-less `get`. This supports structured bindings and generic code that uses tuple traits with ADL `get`. A structured binding over a singular copack, including a singular choice's `value()`, now binds its alternative. Previously, it bound the public `data` and `index` members.
Expand Down
4 changes: 2 additions & 2 deletions TYPE_ALGEBRA.md
Original file line number Diff line number Diff line change
Expand Up @@ -1040,14 +1040,14 @@ The library respects C++ value mechanics:
- `noexcept` is conditionally computed.
- Value categories (lvalue/rvalue) propagate strictly to callbacks, avoiding copies.
- Immovable and move-only payloads are supported in place.
- Reference-bearing `pack<T&...>` and `optional<T&>` are supported. Lifetime management of non-owning references remains with the caller.
- Reference-bearing `pack<T&...>`, `optional<T&>` and `just<T&>` are supported. Lifetime management of non-owning references remains with the caller.
- `pack` compares element-wise, supporting equality and three-way comparison. For reference-bearing `pack<T&...>`, comparison applies to the referents rather than the references themselves.

> [!NOTE]
>
> ### Note — reference payloads
>
> Raw reference payloads are disallowed on the carriers `expected`, `just` and `choice`, and as `copack` alternatives. `expected` stores its payload in a union, and C++ forbids a union member of reference type; the algebra's own types refuse them so that every alternative is dispatched the same way, whatever it holds. `optional<T&>` is the deliberate exception — the standard specifies it, and `libfn` polyfills it. If you want to propagate references inside the other carriers, wrap them in a `pack` (e.g. `expected<pack<T&>, E>`).
> Raw reference payloads are disallowed on the carriers `expected` and `choice`, and as `copack` alternatives. `expected` stores its payload in a union, and C++ forbids a union member of reference type. `copack` excludes reference alternatives so that alternatives follow the same dispatch rules. `optional<T&>` and its always-engaged counterpart, `just<T&>`, support lvalue-reference payloads. Assignment rebinds the reference. Callable operations use `T&` regardless of the carrier's value category or constness, expanding tuple-like referents as `fn::apply` does. `libfn` polyfills the standard's `optional<T&>`. Both exclude references to packs and copacks: how a reference to a product or a sum would take part in conjunction, disjunction and grading is an open question. An owning `just` whose `transform` callable returns an lvalue reference produces a reference `just` only if the carrier is an lvalue; an rvalue carrier rejects it, as the reference may refer into the payload that expires with the carrier. `optional<T>` accepts it, as the standard specifies. If you want to propagate references inside the other carriers, wrap them in a `pack` (e.g. `expected<pack<T&>, E>`).

<!-- sync-example-test-references -->
```cpp
Expand Down
119 changes: 119 additions & 0 deletions docs/reference/just.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ title: "monad fn::just"

:include-doxygen-doc: fn::just

:include-doxygen-doc: fn::just< T & >

## Member types {style: "api"}

```cpp {title: "fn::just::value_type"}
Expand All @@ -28,6 +30,18 @@ using value_type = void; // (1)

:include-doxygen-doc: fn::just< void >::value_type { args: "" }

```cpp {title: "fn::just< T & >::value_type"}
using value_type = T; // (1)
```

:include-doxygen-doc: fn::just< T & >::value_type { args: "" }

```cpp {title: "fn::just< T & >::p_"}
T * p_; // (1)
```

:include-doxygen-doc: fn::just< T & >::p_ { args: "" }

## Construction {style: "api"}

```cpp {title: "fn::just< void >::just"}
Expand Down Expand Up @@ -69,6 +83,24 @@ constexpr explicit just(std::in_place_type_t<T>, Args &&...args); // (6)

:include-doxygen-doc-params: fn::just::just { args: "::std::in_place_type_t< T >, Args &&...", title: "parameters" }

```cpp {title: "fn::just< T & >::just"}
constexpr just(just const &) = default; // (1)

template <typename U>
constexpr explicit just(U &&u); // (2)
constexpr explicit just(std::in_place_type_t<T &>, U &&u); // (3)
```

:include-doxygen-doc: fn::just< T & >::just { args: "just const &" }

:include-doxygen-doc: fn::just< T & >::just { args: "U &&" }

:include-doxygen-doc-params: fn::just< T & >::just { args: "U &&", title: "parameters" }

:include-doxygen-doc: fn::just< T & >::just { args: "::std::in_place_type_t< T & >, U &&" }

:include-doxygen-doc-params: fn::just< T & >::just { args: "::std::in_place_type_t< T & >, U &&", title: "parameters" }

## Destructor {style: "api"}

```cpp {title: "fn::just::~just"}
Expand All @@ -77,6 +109,12 @@ constexpr ~just() = default; // (1)

:include-doxygen-doc: fn::just::~just { args: "" }

```cpp {title: "fn::just< T & >::~just"}
constexpr ~just() = default; // (1)
```

:include-doxygen-doc: fn::just< T & >::~just { args: "" }

## emplace {style: "api"}

```cpp {title: "fn::just::emplace"}
Expand All @@ -87,6 +125,15 @@ constexpr auto emplace(auto &&...args) -> T &; // (1)

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

```cpp {title: "fn::just< T & >::emplace"}
template <typename U>
constexpr auto emplace(U &&u) -> T &; // (1)
```

:include-doxygen-doc: fn::just< T & >::emplace { args: "U &&" }

:include-doxygen-doc-params: fn::just< T & >::emplace { args: "U &&", title: "parameters" }

## Assignment {style: "api"}

```cpp {title: "fn::just::operator="}
Expand All @@ -105,6 +152,12 @@ constexpr auto operator=(U &&v) -> just &; // (3)

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

```cpp {title: "fn::just< T & >::operator="}
constexpr auto operator=(just const &) = default -> just &; // (1)
Comment thread
Bronek marked this conversation as resolved.
```

:include-doxygen-doc: fn::just< T & >::operator= { args: "just const &" }

## operator== {style: "api"}

```cpp {title: "fn::just< void >::operator=="}
Expand Down Expand Up @@ -134,6 +187,14 @@ constexpr auto value() const -> void; // (1)

:include-doxygen-doc: fn::just< void >::value { args: "" }

```cpp {title: "fn::just< T & >::value"}
constexpr auto value() const -> T &; // (1)
```

:include-doxygen-doc: fn::just< T & >::value { args: "" }

:include-doxygen-doc-params: fn::just< T & >::value { args: "", title: "parameters" }

## transform {style: "api"}

```cpp {title: "fn::just::transform"}
Expand All @@ -157,6 +218,15 @@ constexpr auto transform(Fn &&fn) const; // (1)

:include-doxygen-doc-params: fn::just< void >::transform { args: "Fn &&", title: "parameters" }

```cpp {title: "fn::just< T & >::transform"}
template <typename Fn>
constexpr auto transform(Fn &&fn) const; // (1)
```

:include-doxygen-doc: fn::just< T & >::transform { args: "Fn &&" }

:include-doxygen-doc-params: fn::just< T & >::transform { args: "Fn &&", title: "parameters" }

## and_then {style: "api"}

```cpp {title: "fn::just::and_then"}
Expand All @@ -180,6 +250,15 @@ constexpr auto and_then(Fn &&fn) const; // (1)

:include-doxygen-doc-params: fn::just< void >::and_then { args: "Fn &&", title: "parameters" }

```cpp {title: "fn::just< T & >::and_then"}
template <typename Fn>
constexpr auto and_then(Fn &&fn) const; // (1)
```

:include-doxygen-doc: fn::just< T & >::and_then { args: "Fn &&" }

:include-doxygen-doc-params: fn::just< T & >::and_then { args: "Fn &&", title: "parameters" }

## apply {style: "api"}

```cpp {title: "fn::just::apply"}
Expand All @@ -203,6 +282,15 @@ constexpr auto apply(Fn &&fn, Args &&...args) const -> decltype(auto); // (1)

:include-doxygen-doc-params: fn::just< void >::apply { args: "Fn &&, Args &&...", title: "parameters" }

```cpp {title: "fn::just< T & >::apply"}
template <typename Fn, typename... Args>
constexpr auto apply(Fn &&fn, Args &&...args) const -> decltype(auto); // (1)
```

:include-doxygen-doc: fn::just< T & >::apply { args: "Fn &&, Args &&..." }

:include-doxygen-doc-params: fn::just< T & >::apply { args: "Fn &&, Args &&...", title: "parameters" }

## apply_r {style: "api"}

```cpp {title: "fn::just::apply_r"}
Expand Down Expand Up @@ -230,6 +318,17 @@ constexpr auto apply_r(Fn &&fn, Args &&...args) const -> Ret; // (1)

:include-doxygen-doc-params: fn::just< void >::apply_r { args: "Fn &&, Args &&...", title: "parameters" }

```cpp {title: "fn::just< T & >::apply_r"}
template <typename Ret, typename Fn, typename... Args>
constexpr auto apply_r(Fn &&fn, Args &&...args) const -> Ret; // (1)
```

:include-doxygen-doc: fn::just< T & >::apply_r { args: "Fn &&, Args &&..." }

:include-doxygen-doc-params: fn::just< T & >::apply_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" }

:include-doxygen-doc-params: fn::just< T & >::apply_r { args: "Fn &&, Args &&...", title: "parameters" }

## apply_type {style: "api"}

```cpp {title: "fn::just::apply_type"}
Expand All @@ -253,6 +352,15 @@ constexpr auto apply_type(Fn &&fn, Args &&...args) const -> decltype(auto); //

:include-doxygen-doc-params: fn::just< void >::apply_type { args: "Fn &&, Args &&...", title: "parameters" }

```cpp {title: "fn::just< T & >::apply_type"}
template <typename Fn, typename... Args>
constexpr auto apply_type(Fn &&fn, Args &&...args) const -> decltype(auto); // (1)
```

:include-doxygen-doc: fn::just< T & >::apply_type { args: "Fn &&, Args &&..." }

:include-doxygen-doc-params: fn::just< T & >::apply_type { args: "Fn &&, Args &&...", title: "parameters" }

## apply_type_r {style: "api"}

```cpp {title: "fn::just::apply_type_r"}
Expand All @@ -279,3 +387,14 @@ constexpr auto apply_type_r(Fn &&fn, Args &&...args) const -> Ret; // (1)
:include-doxygen-doc-params: fn::just< void >::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" }

:include-doxygen-doc-params: fn::just< void >::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" }

```cpp {title: "fn::just< T & >::apply_type_r"}
template <typename Ret, typename Fn, typename... Args>
constexpr auto apply_type_r(Fn &&fn, Args &&...args) const -> Ret; // (1)
```

:include-doxygen-doc: fn::just< T & >::apply_type_r { args: "Fn &&, Args &&..." }

:include-doxygen-doc-params: fn::just< T & >::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" }

:include-doxygen-doc-params: fn::just< T & >::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" }
9 changes: 9 additions & 0 deletions include/fn/copack.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ template <typename T> struct superset;
template <typename... Ts> struct superset<typelist<Ts...>> {
using type = ::fn::just<typename ::fn::detail::normalized<Ts...>::template apply<::fn::copack>>;
};

template <typename T> constexpr inline bool is_reference_just = false;
template <typename T> constexpr inline bool is_reference_just<::fn::just<T &>> = true;
} // namespace _joining_superset

// Rs are the branch results, cv/ref stripped
Expand All @@ -220,6 +223,12 @@ template <typename R0, typename... Rs>
struct _joining_superset_type<R0, Rs...> {
using type = R0;
};
// References cannot be choice alternatives. If any branch returns a reference just,
// all branches must return the same carrier type, as in optional joins.
template <typename R0, typename... Rs>
requires(not(... && ::std::is_same_v<R0, Rs>))
&& (_joining_superset::is_reference_just<R0> || ... || _joining_superset::is_reference_just<Rs>)
struct _joining_superset_type<R0, Rs...> {};

// An all-just result set uses the join above. Other results must agree in exact type;
// the member then rejects non-just results. Divergent results trigger select's assertion.
Expand Down
Loading
Loading