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 4 commits into
base: develop
Choose a base branch
from
Open
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
38 changes: 38 additions & 0 deletions src/DotNext/Optional.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace DotNext;

using System.Threading.Tasks;
using DotNext.Threading.Tasks;
using Runtime.CompilerServices;
using Intrinsics = Runtime.Intrinsics;

Expand Down Expand Up @@ -79,6 +81,16 @@ public static async Task<Optional<TOutput>> Convert<TInput, TOutput>(this Task<O
: Optional<TOutput>.None;
}

/// <summary>
/// Creates <see cref="Result{T, TError}"/> from <see cref="Optional{T}"/> instance.
/// </summary>
/// <param name="optional">The 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 Result<T, TError> ToResult<T, TError>(this in Optional<T> optional, TError error)
where TError : struct, Enum
=> optional.HasValue ? new(optional.Value) : new(error);

/// <summary>
/// If a value is present, returns the value, otherwise throw exception.
/// </summary>
Expand Down Expand Up @@ -609,6 +621,32 @@ public Optional<TResult> Convert<TResult>(Converter<T, Optional<TResult>> mapper
public unsafe Optional<TResult> Convert<TResult>(delegate*<T, Optional<TResult>> mapper)
=> ConvertOptional<TResult, Supplier<T, Optional<TResult>>>(mapper);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Task<Optional<TResult>> ConvertOptionalTask<TResult, TConverter>(TConverter converter)
where TConverter : struct, ISupplier<T, Task<Optional<TResult>>>
=> HasValue ? converter.Invoke(value) : Task.FromResult(Optional<TResult>.None);

/// <summary>
/// If a value is present, apply the provided mapping function to it, and if the result is
/// non-null, return an Optional describing the result. Otherwise, returns <see cref="None"/>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the mapping function.</typeparam>
/// <param name="mapper">A mapping function to be applied to the value, if present.</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>(Converter<T, Task<Optional<TResult>>> mapper)
Copy link
Collaborator

Choose a reason for hiding this comment

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

How is it differ from existing Optional.Convert extension method? It allows conversion from TInput to Optional<TOutput>. Moreover, there is always Flatten method that you can insert in the beginning of your chain to convert Task<Optional<T>> to plain Task<T>. Then, you can finalize your chain with SuspendException that returns Result<T> with implicit conversion to Optional<T>. Could you provide real-world example that demonstrates the real need of this conversion?

=> ConvertOptionalTask<TResult, DelegatingConverter<T, Task<Optional<TResult>>>>(mapper);

/// <summary>
/// If a value is present, apply the provided mapping function to it, and if the result is
/// non-null, return an Optional describing the result. Otherwise returns <see cref="None"/>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the mapping function.</typeparam>
/// <param name="mapper">A mapping function to be applied to the value, if present.</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>
[CLSCompliant(false)]
public unsafe Task<Optional<TResult>> Convert<TResult>(delegate*<T, Task<Optional<TResult>>> mapper)
=> ConvertOptionalTask<TResult, Supplier<T, Task<Optional<TResult>>>>(mapper);

/// <inheritdoc cref="IFunctional{TDelegate}.ToDelegate()"/>
Func<object?> IFunctional<Func<object?>>.ToDelegate() => Func.Constant<object?>(kind is NotEmptyValue ? value : null);

Expand Down
114 changes: 114 additions & 0 deletions src/DotNext/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

namespace DotNext;

using System;
using DotNext.Threading.Tasks;
using Runtime.CompilerServices;
using Intrinsics = Runtime.Intrinsics;

Expand Down Expand Up @@ -202,6 +204,53 @@ private Result<TResult> Convert<TResult, TConverter>(TConverter converter)
return result;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Result<TResult> ConvertResult<TResult, TConverter>(TConverter converter)
where TConverter : struct, ISupplier<T, Result<TResult>>
{
Result<TResult> result;
if (exception is null)
{
try
{
result = converter.Invoke(value);
}
catch (Exception e)
{
result = new(e);
}
}
else
{
result = new(exception);
}

return result;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private AwaitableResult<TResult> ConvertTask<TResult>(Func<T, CancellationToken, Task<TResult>> converter, CancellationToken token = default)
{
AwaitableResult<TResult> result;
if (exception is null)
{
try
{
result = converter.Invoke(value, token).SuspendException();
}
catch (Exception e)
{
result = new(Task.FromException<TResult>(e));
}
}
else
{
result = new(Task.FromException<TResult>(exception.SourceException));
}

return result;
}

/// <summary>
/// If the successful result is present, apply the provided mapping function hiding any exception
/// caused by the converter.
Expand All @@ -212,6 +261,27 @@ private Result<TResult> Convert<TResult, TConverter>(TConverter converter)
public Result<TResult> Convert<TResult>(Converter<T, TResult> converter)
=> Convert<TResult, DelegatingConverter<T, TResult>>(converter);

/// <summary>
/// If successful result is present, apply the provided mapping function. If not,
/// forward the exception.
/// </summary>
/// <param name="converter">A mapping function to be applied to the value, if present.</param>
/// <typeparam name="TResult">The type of the result of the mapping function.</typeparam>
/// <returns>The conversion result.</returns>
public Result<TResult> Convert<TResult>(Converter<T, Result<TResult>> converter)
=> ConvertResult<TResult, DelegatingConverter<T, Result<TResult>>>(converter);

/// <summary>
/// If successful result is present, apply the provided mapping function. If not,
/// forward the exception.
/// </summary>
/// <param name="converter">A mapping function to be applied to the value, if present.</param>
/// <typeparam name="TResult">The type of the result of the mapping function.</typeparam>
/// <param name="token">The token that can be used to cancel the operation.</param>
/// <returns>The conversion result.</returns>
public AwaitableResult<TResult> Convert<TResult>(Func<T, CancellationToken, Task<TResult>> converter, 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.

AwaitableResult<T> can be obtained with SuspendException in the end of the chain, without any extra methods.

=> ConvertTask(converter, token);

/// <summary>
/// If the successful result is present, apply the provided mapping function hiding any exception
/// caused by the converter.
Expand All @@ -223,6 +293,17 @@ public Result<TResult> Convert<TResult>(Converter<T, TResult> converter)
public unsafe Result<TResult> Convert<TResult>(delegate*<T, TResult> converter)
=> Convert<TResult, Supplier<T, TResult>>(converter);

/// <summary>
/// If successful result is present, apply the provided mapping function. If not,
/// forward the exception.
/// </summary>
/// <param name="converter">A mapping function to be applied to the value, if present.</param>
/// <typeparam name="TResult">The type of the result of the mapping function.</typeparam>
/// <returns>The conversion result.</returns>
[CLSCompliant(false)]
public unsafe Result<TResult> Convert<TResult>(delegate*<T, Result<TResult>> converter)
=> ConvertResult<TResult, Supplier<T, Result<TResult>>>(converter);

/// <summary>
/// Attempts to extract value from the container if it is present.
/// </summary>
Expand Down Expand Up @@ -323,6 +404,13 @@ public ValueTask<T> AsTask()
{ } error => ValueTask.FromException<T>(error),
};

/// <summary>
/// Converts this result to <see cref="AwaitableResult{TResult}"/>.
/// </summary>
/// <returns>The awaitable Result representing the result.</returns>
public AwaitableResult<T> ToAwaitable()
=> IsSuccessful ? new(Task.FromResult(value)) : new(Task.FromException<T>(Error));

/// <summary>
/// Converts the result to <see cref="Task{TResult}"/>.
/// </summary>
Expand Down Expand Up @@ -534,6 +622,11 @@ private Result<TResult, TError> Convert<TResult, TConverter>(TConverter converte
where TConverter : struct, ISupplier<T, TResult>
=> IsSuccessful ? new(converter.Invoke(value)) : new(Error);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Result<TResult, TError> ConvertResult<TResult, TConverter>(TConverter converter)
where TConverter : struct, ISupplier<T, Result<TResult, TError>>
=> IsSuccessful ? converter.Invoke(value) : new(Error);

/// <summary>
/// If the successful result is present, apply the provided mapping function hiding any exception
/// caused by the converter.
Expand All @@ -544,6 +637,16 @@ private Result<TResult, TError> Convert<TResult, TConverter>(TConverter converte
public Result<TResult, TError> Convert<TResult>(Converter<T, TResult> converter)
=> Convert<TResult, DelegatingConverter<T, TResult>>(converter);

/// <summary>
/// If successful result is present, apply the provided mapping function. If not,
/// forward the error.
/// </summary>
/// <param name="converter">A mapping function to be applied to the value, if present.</param>
/// <typeparam name="TResult">The type of the result of the mapping function.</typeparam>
/// <returns>The conversion result.</returns>
public Result<TResult, TError> Convert<TResult>(Converter<T, Result<TResult, TError>> converter)
=> ConvertResult<TResult, DelegatingConverter<T, Result<TResult, TError>>>(converter);

/// <summary>
/// If the successful result is present, apply the provided mapping function hiding any exception
/// caused by the converter.
Expand All @@ -555,6 +658,17 @@ public Result<TResult, TError> Convert<TResult>(Converter<T, TResult> converter)
public unsafe Result<TResult, TError> Convert<TResult>(delegate*<T, TResult> converter)
=> Convert<TResult, Supplier<T, TResult>>(converter);

/// <summary>
/// If successful result is present, apply the provided mapping function. If not,
/// forward the error.
/// </summary>
/// <param name="converter">A mapping function to be applied to the value, if present.</param>
/// <typeparam name="TResult">The type of the result of the mapping function.</typeparam>
/// <returns>The conversion result.</returns>
[CLSCompliant(false)]
public unsafe Result<TResult, TError> Convert<TResult>(delegate*<T, Result<TResult, TError>> converter)
=> ConvertResult<TResult, Supplier<T, Result<TResult, TError>>>(converter);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private T OrInvoke<TSupplier>(TSupplier defaultFunc)
where TSupplier : struct, ISupplier<T>
Expand Down