Skip to content

Commit

Permalink
feat: Add monadic functions to Result (#3957)
Browse files Browse the repository at this point in the history
In order to make the `Result` type easier to use, this commit adds three new functions:

 * `Result::value_or` allows the user to obtain the value or a provided default value.
 * `Result::transform` models functorial mapping, allowing users to modify values inside results.
 * `Result::and_then` models monadic binding, allowing users to build complex chains of actions on results.

Implemented are lvalue and rvalue versions of these functions as well as tests.

![image](https://github.com/user-attachments/assets/591e64a2-e4fb-4ce6-9d34-166b28f7a837)


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Enhanced `Result` class with new methods for improved error handling and value transformation:
		- `value_or`: Retrieve valid value or default.
		- `transform`: Apply a function to the valid value.
		- `and_then`: Chain functions based on the result's validity.

- **Tests**
	- Added comprehensive test cases to validate the new functionalities of the `Result` class, ensuring reliability and correctness:
		- `ValueOrResult`: Validates the `value_or` method.
		- `TransformResult`: Assesses the `transform` method.
		- `AndThenResult`: Evaluates the `and_then` method.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
stephenswat authored Dec 7, 2024
1 parent f16ed67 commit 9067679
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 0 deletions.
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

0 comments on commit 9067679

Please sign in to comment.