Skip to content

Borrowed choice: should just<copack<Ts...>&> be supported? #434

Description

@Bronek

#417 allows lvalue-reference payloads in just but excludes references to copacks: just<copack<Ts...> cv &> is ill-formed for every Ts.... This issue explains why that restriction is provisional, makes the case for lifting it, and identifies the questions that need answers before a decision.

The case for supporting borrowed choice

fn::optional<copack<Ts...>&> exists today and behaves as a borrowed graded optional: transform dispatches over the referent's active alternative and collapses the branch results into optional<copack_for<...>>, and_then joins, and apply dispatches. Under the current restriction, this optional has no corresponding infallible carrier—the asymmetry #417 removes for every other referent. A just<copack<Ts...>&> with matching behaviour would be a borrowed choice.

Why the answer is not obvious

A reference to a sum is not a sum of references. There is no map from A& to copack<A, B>&, because a free-standing A is not the active alternative of any copack. A reference obtained from copack<A, B>& to its active alternative remains valid only until that alternative is replaced, possibly through another alias. References to a pack behave differently: member access yields references to the fields that stay valid for the pack's lifetime.

The algebra recognises copacks even through references: some_copack is cvref-agnostic, pack refuses a copack& element, and fn::apply and fn::optional dispatch a copack reference as they dispatch a copack. A just<copack&> that treats its referent as an opaque atom would therefore conflict with the rest of the library. The candidate considered here is a borrowed choice that dispatches over the active alternative. It has the following limitations and design questions:

  • Its operations do not preserve the borrowed type. Callbacks see A& or B&, never the copack, so neither transform nor and_then can reconstruct the same borrowed choice from the callback argument alone; transform with an identity callback yields an owning choice<Ts...>, equal in value but not in type or referent. A borrowed pack shares this, since the engine hands the callback its fields: just<pack<int>&> maps to just<int&>, never back to itself. What separates the two is lifetime. A reference to a field stays valid for the pack's lifetime, while a reference to an alternative dangles once the referent switches alternative.
  • The construction of choice as a monad in Section 11 of TYPE_ALGEBRA does not carry over. There, building a choice from one alternative composes the unit with the coproduct's injection; a borrowed choice has no injection, so left identity cannot be stated, and right identity holds only up to a copy.
  • Its control flow lives in the referent. A borrowed choice branches on its referent's discriminant, so a const carrier can take different branches on successive calls. fn::optional<copack<Ts...>&> already behaves this way; admitting a borrowed choice would extend that behaviour, which is itself part of this question, to the identity carriers.
  • Two operations need a choice of semantics. emplace rebinds on just<T&> and optional<T&> but replaces the active alternative on choice. apply_type tags the payload type on just (in_place_type<T>), the active alternative on choice (in_place_type<T_i>), and the state on optional (in_place).
  • Allowing reference alternatives in copack would introduce a further lifetime hazard. transform with a callback returning A&/B& would then produce choice<A&, B&>, a sum of references into the referent's alternatives. The stored reference would dangle when the referent switches alternatives.

The empty case is independent of this decision. No object of the uninhabited copack<> exists, so there can be no valid reference to one. just<copack<> &> is uninhabited for the same reason choice<> is incomplete and remains unsupported regardless of the outcome here.

Compatibility with pfn::optional needs resolving

The behaviour of fn::optional<copack<Ts...>&> follows from some_copack being cvref-agnostic; it was not an explicit design decision. It differs from pfn::optional, breaking the rule that switching a valid program from pfn to fn preserves both compilation and behaviour. The by-value fn::optional<copack<Ts...>> also differs when a callable takes the copack as a whole.

The following reproducer compiles cleanly on g++ 16.2.1 and clang++ 22.1.8 with -std=c++20 -Wall -Wextra -Werror:

#include <fn/copack.hpp>
#include <fn/optional.hpp>
#include <pfn/optional.hpp>

#include <concepts>
#include <utility>

struct A {};
struct B {};
using AB = fn::copack_for<A, B>;

constexpr auto whole = [](AB const &) { return 42; };
constexpr auto whole_ref = [](AB &c) -> AB & { return c; };

template <typename O, typename F>
concept transformable = requires(O o, F f) { o.transform(f); };

// A callable taking the copack whole: pfn maps to its result, fn wraps it in a copack
static_assert(std::same_as<decltype(std::declval<pfn::optional<AB &>>().transform(whole)), pfn::optional<int>>);
static_assert(std::same_as<decltype(std::declval<fn::optional<AB &>>().transform(whole)), fn::optional<fn::copack<int>>>);
static_assert(std::same_as<decltype(std::declval<pfn::optional<AB>>().transform(whole)), pfn::optional<int>>);
static_assert(std::same_as<decltype(std::declval<fn::optional<AB>>().transform(whole)), fn::optional<fn::copack<int>>>);

// A callable returning the copack reference: pfn keeps the view, fn refuses the call
static_assert(std::same_as<decltype(std::declval<pfn::optional<AB &>>().transform(whole_ref)), pfn::optional<AB &>>);
static_assert(not transformable<fn::optional<AB &>, decltype(whole_ref)>);

int main() {}

tests/fn/optional_polyfill.cpp does not catch this: the pfn suite it replays never puts a copack in an optional.

Questions to resolve before deciding

  • What should each operation of optional<copack<Ts...>&> do to preserve the meaning of pfn programs? Can its graded behaviour coexist with that requirement? This needs to cover construction, assignment, emplace, value, transform, and_then, or_else, the apply family, comparisons, &, and |.
  • Should a borrowed choice provide transform and and_then if they cannot preserve the borrowed type from the callback argument alone? cp.transform(f) and fn::apply(f, cp) already operate directly on a copack lvalue.
  • Which semantics should emplace and apply_type have on a borrowed choice?
  • Is supporting borrowed choice worth a third just specialization that implements choice dispatch over pointer storage?

Until these questions are resolved, the restriction remains. The alternatives are fn::as_choice(cp), which copies into an owning choice, and just<choice<Ts...>&>, which borrows a whole choice and treats it as an atom in every operation.

Assisted-by: Claude:claude-opus-5-5

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrelease-0.2Planned for release 0.2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions