Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add monadic functions to Result #3957

Merged
merged 4 commits into from
Dec 7, 2024
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
141 changes: 141 additions & 0 deletions Core/include/Acts/Utilities/Result.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class Result {
Result(std::variant<T, E>&& var) : m_var(std::move(var)) {}

public:
using ValueType = T;
using ErrorType = E;

/// Default construction is disallowed.
Result() = delete;

Expand Down Expand Up @@ -172,6 +175,144 @@ class Result {
return std::move(std::get<T>(m_var));
}

/// Retrieves the valid value from the result object, or returns a default
/// value if no valid value exists.
///
/// @param[in] v The default value to use if no valid value exists.
/// @note This is the lvalue version.
/// @note This function always returns by value.
/// @return Either the valid value, or the given substitute.
template <typename U>
std::conditional_t<std::is_reference_v<U>, const T&, T> value_or(U&& v) const&
requires(std::same_as<std::decay_t<U>, T>)
{
if (ok()) {
return value();
} else {
return std::forward<U>(v);
}
}

/// Retrieves the valid value from the result object, or returns a default
/// value if no valid value exists.
///
/// @param[in] v The default value to use if no valid value exists.
/// @note This is the rvalue version which moves the value out.
/// @note This function always returns by value.
/// @return Either the valid value, or the given substitute.
template <typename U>
T value_or(U&& v) &&
requires(std::same_as<std::decay_t<U>, T>)
{
if (ok()) {
return std::move(*this).value();
} else {
return std::forward<U>(v);
}
}

/// Transforms the value contained in this result.
///
/// Applying a function `f` to a valid value `x` returns `f(x)`, while
/// applying `f` to an invalid value returns another invalid value.
///
/// @param[in] callable The transformation function to apply.
/// @note This is the lvalue version.
/// @note This functions is `fmap` on the functor in `A` of `Result<A, E>`.
/// @return The modified valid value if exists, or an error otherwise.
template <typename C>
auto transform(C&& callable) const&
requires std::invocable<C, const T&>
{
using CallableReturnType = decltype(std::declval<C>()(std::declval<T>()));
using R = Result<std::decay_t<CallableReturnType>, E>;
if (ok()) {
return R::success(callable(value()));
} else {
return R::failure(error());
}
}

/// Transforms the value contained in this result.
///
/// Applying a function `f` to a valid value `x` returns `f(x)`, while
/// applying `f` to an invalid value returns another invalid value.
///
/// @param[in] callable The transformation function to apply.
/// @note This is the rvalue version.
/// @note This functions is `fmap` on the functor in `A` of `Result<A, E>`.
/// @return The modified valid value if exists, or an error otherwise.
template <typename C>
auto transform(C&& callable) &&
requires std::invocable<C, T&&>
{
using CallableReturnType = decltype(std::declval<C>()(std::declval<T>()));
using R = Result<std::decay_t<CallableReturnType>, E>;
if (ok()) {
return R::success(callable(std::move(*this).value()));
} else {
return R::failure(std::move(*this).error());
}
}

/// Bind a function to this result monadically.
///
/// This function takes a function `f` and, if this result contains a valid
/// value `x`, returns `f(x)`. If the type of `x` is `T`, then `f` is
/// expected to accept type `T` and return `Result<U>`. In this case,
/// `transform` would return the unhelpful type `Result<Result<U>>`, so
/// `and_then` strips away the outer layer to return `Result<U>`. If the
/// value is invalid, this returns an invalid value in `Result<U>`.
///
/// @param[in] callable The transformation function to apply.
/// @note This is the lvalue version.
/// @note This functions is `>>=` on the functor in `A` of `Result<A, E>`.
/// @return The modified valid value if exists, or an error otherwise.
template <typename C>
auto and_then(C&& callable) const&
requires std::invocable<C, const T&>
{
using R = decltype(std::declval<C>()(std::declval<T>()));

static_assert(std::same_as<typename R::ErrorType, ErrorType>,
"bind must take a callable with the same error type");

if (ok()) {
return callable(value());
} else {
return R::failure(error());
}
}

/// Bind a function to this result monadically.
///
/// This function takes a function `f` and, if this result contains a valid
/// value `x`, returns `f(x)`. If the type of `x` is `T`, then `f` is
/// expected to accept type `T` and return `Result<U>`. In this case,
/// `transform` would return the unhelpful type `Result<Result<U>>`, so
/// `and_then` strips away the outer layer to return `Result<U>`. If the
/// value is invalid, this returns an invalid value in `Result<U>`.
///
/// @param[in] callable The transformation function to apply.
/// @note This is the rvalue version.
/// @note This functions is `>>=` on the functor in `A` of `Result<A, E>`.
/// @return The modified valid value if exists, or an error otherwise.
template <typename C>
auto and_then(C&& callable) &&
requires std::invocable<C, T&&>
{
using R = decltype(std::declval<C>()(std::declval<T>()));

static_assert(std::same_as<typename R::ErrorType, ErrorType>,
"bind must take a callable with the same error type");

if (ok()) {
return callable(std::move(*this).value());
} else {
return R::failure(std::move(*this).error());
}
}

private:
std::variant<T, E> m_var;

Expand Down
88 changes: 88 additions & 0 deletions Tests/UnitTests/Core/Utilities/ResultTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,94 @@ BOOST_AUTO_TEST_CASE(BoolResult) {
BOOST_CHECK_EQUAL(res.error(), MyError::Failure);
}

BOOST_AUTO_TEST_CASE(ValueOrResult) {
using Result = Result<int>;

Result res = Result::success(5);
BOOST_CHECK_EQUAL(res.value_or(42), 5);

res = Result::failure(MyError::Failure);
BOOST_CHECK_EQUAL(res.value_or(42), 42);

BOOST_CHECK_EQUAL(Result::success(5).value_or(42), 5);
BOOST_CHECK_EQUAL(Result::failure(MyError::Failure).value_or(42), 42);

int val = 25;
const int cval = 30;

BOOST_CHECK_EQUAL(Result::success(5).value_or(val), 5);
BOOST_CHECK_EQUAL(Result::success(5).value_or(cval), 5);
BOOST_CHECK_EQUAL(Result::failure(MyError::Failure).value_or(val), 25);
BOOST_CHECK_EQUAL(Result::failure(MyError::Failure).value_or(cval), 30);

res = Result::success(5);

BOOST_CHECK_EQUAL(res.value_or(val), 5);
BOOST_CHECK_EQUAL(&(res.value_or(val)), &res.value());
BOOST_CHECK_EQUAL(res.value_or(cval), 5);
BOOST_CHECK_EQUAL(&(res.value_or(cval)), &res.value());

res = Result::failure(MyError::Failure);

BOOST_CHECK_EQUAL(res.value_or(val), 25);
BOOST_CHECK_EQUAL(res.value_or(cval), 30);
BOOST_CHECK_EQUAL(&(res.value_or(val)), &val);
BOOST_CHECK_EQUAL(&(res.value_or(cval)), &cval);
}

BOOST_AUTO_TEST_CASE(TransformResult) {
using Result = Result<int>;

auto f1 = [](int x) { return 2 * x; };

Result res = Result::success(5);
Result res2 = res.transform(f1);
BOOST_CHECK(res2.ok());
BOOST_CHECK_EQUAL(*res2, 10);

res = Result::failure(MyError::Failure);
res2 = res.transform(f1);
BOOST_CHECK(!res2.ok());

BOOST_CHECK(Result::success(5).transform(f1).ok());
BOOST_CHECK_EQUAL(Result::success(5).transform(f1).value(), 10);

BOOST_CHECK(!Result::failure(MyError::Failure).transform(f1).ok());
}

BOOST_AUTO_TEST_CASE(AndThenResult) {
using Result1 = Result<int>;
using Result2 = Result<std::string>;

auto f1 = [](int x) -> Result2 {
return Result2::success("hello " + std::to_string(x));
};
auto f2 = [](int) -> Result2 { return Result2::failure(MyError::Failure); };

Result1 res = Result1::success(5);
Result2 res2 = res.and_then(f1);
BOOST_CHECK(res2.ok());
BOOST_CHECK_EQUAL(*res2, "hello 5");

res2 = res.and_then(f2);
BOOST_CHECK(!res2.ok());

res = Result1::failure(MyError::Failure);
res2 = res.and_then(f1);
BOOST_CHECK(!res2.ok());

res2 = res.and_then(f2);
BOOST_CHECK(!res2.ok());

BOOST_CHECK(Result1::success(5).and_then(f1).ok());
BOOST_CHECK_EQUAL(Result1::success(5).and_then(f1).value(), "hello 5");

BOOST_CHECK(!Result1::success(5).and_then(f2).ok());

BOOST_CHECK(!Result1::failure(MyError::Failure).and_then(f1).ok());

BOOST_CHECK(!Result1::failure(MyError::Failure).and_then(f2).ok());
}
BOOST_AUTO_TEST_SUITE_END()

} // namespace Acts::Test
Loading