Skip to content

Commit 8e92456

Browse files
authored
Add compose_first_error_and_result, closes #265 (#329)
Pairs a possibly-failing side-effecting function (e.g. logging, metrics, validation, audit, cache lookup) with a regular result-returning function. Both are called on the same input; the second runs regardless of whether the first one failed. The error of the first is preserved as a Maybe alongside the second's unmodified Result.
1 parent 961d0e9 commit 8e92456

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

include/fplus/result.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,25 @@ auto compose_result(Callables&&... callables)
346346
return internal::compose_binary_lift(bind_result,
347347
std::forward<Callables>(callables)...);
348348
}
349+
350+
// API search type: compose_first_error_and_result : ((a -> Result b c), (a -> Result d e)) -> (a -> (Maybe c, Result d e))
351+
// Run two result-returning functions on the same input independently.
352+
// The first contributes only its potential error (as a Maybe), its Ok
353+
// value is discarded. The second's result is forwarded unchanged.
354+
// The second function is always called, regardless of whether the first
355+
// one failed. Useful for ancillary checks (logging, metrics, validation,
356+
// audit, cache lookup, ...) that may fail but should not block the main
357+
// computation.
358+
template <typename First, typename Second>
359+
auto compose_first_error_and_result(First first, Second second)
360+
{
361+
return [first = std::move(first), second = std::move(second)](const auto& x) {
362+
auto first_result = internal::invoke(first, x);
363+
using FirstErr = typename std::decay_t<decltype(first_result)>::error_t;
364+
auto first_err = is_error(first_result)
365+
? just<FirstErr>(unsafe_get_error(first_result))
366+
: nothing<FirstErr>();
367+
return std::make_pair(std::move(first_err), internal::invoke(second, x));
368+
};
369+
}
349370
} // namespace fplus

include_all_in_one/include/fplus/fplus.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5719,6 +5719,27 @@ auto compose_result(Callables&&... callables)
57195719
return internal::compose_binary_lift(bind_result,
57205720
std::forward<Callables>(callables)...);
57215721
}
5722+
5723+
// API search type: compose_first_error_and_result : ((a -> Result b c), (a -> Result d e)) -> (a -> (Maybe c, Result d e))
5724+
// Run two result-returning functions on the same input independently.
5725+
// The first contributes only its potential error (as a Maybe), its Ok
5726+
// value is discarded. The second's result is forwarded unchanged.
5727+
// The second function is always called, regardless of whether the first
5728+
// one failed. Useful for ancillary checks (logging, metrics, validation,
5729+
// audit, cache lookup, ...) that may fail but should not block the main
5730+
// computation.
5731+
template <typename First, typename Second>
5732+
auto compose_first_error_and_result(First first, Second second)
5733+
{
5734+
return [first = std::move(first), second = std::move(second)](const auto& x) {
5735+
auto first_result = internal::invoke(first, x);
5736+
using FirstErr = typename std::decay_t<decltype(first_result)>::error_t;
5737+
auto first_err = is_error(first_result)
5738+
? just<FirstErr>(unsafe_get_error(first_result))
5739+
: nothing<FirstErr>();
5740+
return std::make_pair(std::move(first_err), internal::invoke(second, x));
5741+
};
5742+
}
57225743
} // namespace fplus
57235744

57245745
#include <algorithm>

test/result_test.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,34 @@ TEST_CASE("result_test - compose_result")
116116
REQUIRE_EQ(squareSumResult(5, 5), (ok<int, std::string>(100)));
117117
}
118118

119+
TEST_CASE("result_test - compose_first_error_and_result")
120+
{
121+
using namespace fplus;
122+
123+
const auto check_ok = [](int) {
124+
return ok<int, std::string>(0);
125+
};
126+
const auto check_fail = [](int) {
127+
return error<int, std::string>("check failed");
128+
};
129+
const auto run = [](int x) {
130+
return x < 0
131+
? error<int, std::string>("negative input")
132+
: ok<int, std::string>(x * x);
133+
};
134+
135+
using Pair = std::pair<maybe<std::string>, result<int, std::string>>;
136+
137+
REQUIRE_EQ(compose_first_error_and_result(check_ok, run)(3),
138+
Pair(nothing<std::string>(), ok<int, std::string>(9)));
139+
REQUIRE_EQ(compose_first_error_and_result(check_ok, run)(-1),
140+
Pair(nothing<std::string>(), error<int, std::string>("negative input")));
141+
REQUIRE_EQ(compose_first_error_and_result(check_fail, run)(3),
142+
Pair(just<std::string>("check failed"), ok<int, std::string>(9)));
143+
REQUIRE_EQ(compose_first_error_and_result(check_fail, run)(-1),
144+
Pair(just<std::string>("check failed"), error<int, std::string>("negative input")));
145+
}
146+
119147
TEST_CASE("result_test - lift")
120148
{
121149
using namespace fplus;

0 commit comments

Comments
 (0)