Skip to content

Added convenience methods for easier Monad creation and Monad chaining #258

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

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from

Conversation

julianthurner
Copy link

@julianthurner julianthurner commented Feb 4, 2025

I added some convenience methods to the library. The Option class receives a few extension methods that allow for chaining async conversions:

var optional = await Optional.FromValue(42)
    .Convert(async x => x > 10 ? Optional.FromValue(x * 2.0 - 20) : Optional<double>.None)
    .Convert(async x => x > 0 ? Optional.FromValue("Success") : Optional<string>.None);

Console.WriteLine(optional.HasValue ? optional.Value : "Optional has no value");

This is really useful if you work with databases where data is fetched / transformed multiple times asynchronously before being filled into a DTO.
The same thing for Result:

var result = await Result.FromValue(42)
    .Convert(async x => x > 10 ? Result.FromValue(x * 2.0 - 20) : Result.FromException<double>(new Exception("x too small")))
    .Convert(async x => x > 0 ? Result.FromValue("Success") : Result.FromException<string>(new Exception("x less than zero")));

Console.WriteLine(result.IsSuccessful ? result : $"Failed with exception: {result.Error}");

This also forwards the exception up the chain just like with regular monads.

There's also overloads for converting Option ↔ Result in both directions if there's need for conversion within a chain.

var optionalFromResult = await Task.FromResult(Result.FromValue<int, TestError>(42))
    .TryGet()
    .Convert(async x => x > 10 ? Optional.FromValue(x) : Optional<int>.None);
Console.WriteLine(optionalFromResult.HasValue ? optionalFromResult.Value : "Optional has no value");

var resultFromOptional = await Task.FromResult(Optional.FromValue(42))
    .ToResult()
    .Convert(async x => x > 10 ? Result.FromValue("Success") : Result.FromException<string>(new Exception("x too small")));
Console.WriteLine(resultFromOptional.IsSuccessful ? resultFromOptional.Value : $"Failed with exception: {resultFromOptional.Error}");

Also, I added some additional convenience methods for creating Options and Results statically:

var optionalWithValue = Optional.FromValue(42);
var errorCodeResultWithValue = Result.FromValue<int, TestError>(42);
var errorCodeResultWithError = Result.FromError<int, TestError>(TestError.Error1);

@julianthurner
Copy link
Author

@dotnet-policy-service agree

@dotnet-policy-service agree

@sakno
Copy link
Collaborator

sakno commented Feb 12, 2025

@julianthurner , thanks for your contribution! According to the Contribution Guideline, could you change the target branch from master to develop?

@sakno sakno self-assigned this Feb 12, 2025
@julianthurner julianthurner changed the base branch from master to develop February 12, 2025 13:25
@sakno
Copy link
Collaborator

sakno commented Feb 12, 2025

Changing the target branch is not enough. master cannot be used as a base branch for your branch, because master branch contains only squashed commits from develop branch. You need to create a branch from develop and cherrypick your commits, then force push.

Also, after the discussion of the API, units tests need to be added as well.

@julianthurner julianthurner force-pushed the master branch 2 times, most recently from e1ee1f1 to f58043f Compare February 26, 2025 13:35
@sakno
Copy link
Collaborator

sakno commented Mar 4, 2025

I would suggest to review the proposed API again from your side, since 5.19.0 has been released:

  1. Some factory method ✔️
  2. SuspendException to return Result<T> or Result<T, TError> ✔️
  3. Async Convert with cancellation support ✔️
  4. Flatten to convert Task<Optional<T>> to Task<T> ✔️

@julianthurner
Copy link
Author

I would suggest to review the proposed API again from your side, since 5.19.0 has been released:

  1. Some factory method ✔️
  2. SuspendException to return Result<T> or Result<T, TError> ✔️
  3. Async Convert with cancellation support ✔️

Looks nice👍I will review the changes and adjust them accordingly, sometime this week probably.

@julianthurner
Copy link
Author

Well, life got in the way ...
I started solving the merge conflicts today. But I'm in the middle of exams right now, so it's probably gonna take a little more time till I can continue.

@julianthurner
Copy link
Author

@sakno Sorry it took me this long. My exams were eating up any brain capacity I had, but luckily they are over now. Please take a look at the updated version. I will add Unit Tests once the API is finished.

/// </summary>
/// <param name="task">The task containing Optional value.</param>
/// <returns>The converted optional value.</returns>
public static async Task<Result<T>> ToResult<T>(this Task<Optional<T>> task)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task<Result<T>> is redundant type, this conversion makes no sense. There is Flatten method that converts Task<Optional<T>> to Task<T>.

Copy link
Author

@julianthurner julianthurner Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, I changed the implementation to return AwaitableResult<T> instead.

Copy link
Collaborator

@sakno sakno Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do that, because SuspendException provides the same semantics:

Task<Optional<int>> optional = ...;
Result<int> result = await optional.Flatten().SuspendException();

/// <param name="task">The task containing Optional value.</param>
/// <param name="error">The error code to apply if the value is not present.</param>
/// <returns>The converted optional value.</returns>
public static async Task<Result<T, TError>> ToResult<T, TError>(this Task<Optional<T>> task, TError error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task<Result<T, TError>> is a redundant type.

Copy link
Author

@julianthurner julianthurner Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the implementation to return AwaitableResult<T, TError> instead.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do that, because there is an overload for SuspendException that returns the necessary result:

public static AwaitableResult<T, TError> SuspendException<T, TError>(this Task<T> task,
        Converter<Exception, TError> converter)

/// <param name="mapper">A mapping function to be applied to the value, if present.</param>
/// <param name="token">The token that can be used to cancel the operation.</param>
/// <returns>An Optional describing the result of applying a mapping function to the value of this Optional, if a value is present, otherwise <see cref="None"/>.</returns>
public Task<Optional<TResult>> Convert<TResult>(Func<T, CancellationToken, Task<TResult>> mapper, CancellationToken token = default)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is redundant, since SuspendException returns Result<T> that has implicit conversion to Optional<T>.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is redundant, since SuspendException returns Result<T> that has implicit conversion to Optional<T>.

@sakno I'm not sure I understand. This method is intended to be used like this:

var optional = Optional.Some(42);
var convertedOptional = await optional.Convert(async (int x, CancellationToken y) => x.ToString());

What would be the correct way of implementing this with SuspendException instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same can be achieved with existing extension method:

var optional = Optional.Some(42);
Optional<string> convertedOptional = Task.FromResult(optional).Convert(async (value, token) => x.ToString());

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SuspendException as the last call in the chain always returns Result<T>, so any async chain can be converted to Result<T> or Optional<T> in the end.

Copy link
Collaborator

@sakno sakno Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means that public static async Task<Optional<TOutput>> Convert<TInput, TOutput>(this Task<Optional<TInput>> task, Func<TInput, CancellationToken, Task<Optional<TOutput>>> converter, CancellationToken token = default) method is redundant as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants