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 either extensions #3

Merged
merged 1 commit into from
Feb 21, 2022
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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,32 @@ using LanguageExt.Common;
using Jds.LanguageExt.Extras;
```

## `Result<TSuccess>` Extensions
### `Either<TLeft, TRight>` Extensions

* `Either<TLeft, TRight>.Filter<TLeft, TRight>(Func<TRight, bool> filter, Func<TRight, TLeft> onFalse)`
* Filters right values by executing `filter`. If it returns `true`, the existing value is returned. If it returns `false`, then it executes `onFalse` to create a `TLeft`.
* `Either<TLeft, TRight>.FilterAsync<TLeft, TRight>(Func<TRight, Task<bool>> filter, Func<TRight, TLeft> onFalse)`
* Filters right values by executing `filter`. If it returns `true`, the existing value is returned. If it returns `false`, then it executes `onFalse` to create a `TLeft`.
* `Either<TLeft, TRight>.FilterAsync<TLeft, TRight>(Func<TRight, Task<bool>> filter, Func<TRight, Task<TLeft>> onFalse)`
* Filters right values by executing `filter`. If it returns `true`, the existing value is returned. If it returns `false`, then it executes `onFalse` to create a `TLeft`.
* `Either<TLeft, TRight>.Tap<TLeft, TRight>(Action<TRight> onRight, Action<TLeft> onLeft)`
* Executes a side effect, e.g., logging, based upon the state of the either. The current value is returned unchanged.
* `Either<TLeft, TRight>.TapAsync<TLeft, TRight>(Func<TRight, Task> onSuccess, Func<TLeft, Task> onFailure)`
* Executes an asynchronous side effect, e.g., dispatching a status notification via `HttpClient`, based upon the state of the either. The current value is returned unchanged.
* `Either<TLeft, TRight>.TapLeft<TLeft, TRight>(Action<TLeft> onFailure)`
* Executes a side effect, e.g., logging, when the either is a left. The current value is returned unchanged.
* `Either<TLeft, TRight>.TapLeftAsync<TLeft, TRight>(Func<TLeft, Task> onFailure)`
* Executes an asynchronous side effect, e.g., dispatching a status notification via `HttpClient`, when the either is a left. The current value is returned unchanged.
* `Either<TLeft, TRight>.TapRight<TLeft, TRight>(Action<TRight> onSuccess)`
* Executes a side effect, e.g., logging, when the either is a right. The current value is returned unchanged.
* `Either<TLeft, TRight>.TapRightAsync<TLeft, TRight>(Func<TRight, Task> onSuccess)`
* Executes an asynchronous side effect, e.g., dispatching a status notification via `HttpClient`, when the either is a right. The current value is returned unchanged.
* `Either<TLeft, TRight>.ToResult<TLeft, TRight>(Func<TLeft, Exception> ifLeft)`
* Converts the `Either<TLeft, TRight>` into a `Result<TRight>`.
* `Either<TLeft, TRight>.ToResult<TLeft, TRight>() where TLeft : Exception`
* Converts the `Either<TLeft, TRight>` into a `Result<TRight>`.

### `Result<TSuccess>` Extensions

* `Result<TSuccess>.Bind<TSuccess, TNewSuccess>(Func<TSuccess, Result<TNewSuccess>> func)`
* Executes `func`, which returns `Result<TNewSuccess>`, when the result is a success.
Expand Down
298 changes: 298 additions & 0 deletions src/EitherExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
using LanguageExt;
using LanguageExt.Common;

namespace Jds.LanguageExt.Extras;

/// <summary>
/// Extension methods for <see cref="Either{L,R}" />.
/// </summary>
public static class EitherExtensions
{
/// <summary>
/// Filters right values by executing <paramref name="filter" />. If true, returns the current
/// <typeparamref name="TRight" /> value. If false, executes <paramref name="onFalse" /> to convert the filtered
/// <typeparamref name="TRight" /> to a <typeparamref name="TLeft" />.
/// </summary>
/// <remarks>
/// <para>
/// This overload of <see cref="Either{L,R}.Filter" /> avoids returning <see cref="Either{L,R}.Bottom" />.
/// </para>
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="filter">
/// A filtering function. If true, value continues as-is. If false, value is converted to a
/// <typeparamref name="TLeft" />.
/// </param>
/// <param name="onFalse">
/// A function to convert <typeparamref name="TRight" /> to a <typeparamref name="TLeft" />.
/// </param>
/// <returns>
/// If <paramref name="filter" /> returns true, current <typeparamref name="TRight" /> is returned. If it returns
/// false, the converted <typeparamref name="TLeft" /> is returned.
/// </returns>
public static Either<TLeft, TRight> Filter<TLeft, TRight>(this Either<TLeft, TRight> either,
Func<TRight, bool> filter,
Func<TRight, TLeft> onFalse
)
{
return either.Bind(right =>
filter(right)
? Prelude.Right<TLeft, TRight>(right)
: Prelude.Left<TLeft, TRight>(value: onFalse(right)));
}

/// <summary>
/// Filters right values by executing <paramref name="filter" />. If true, returns the current
/// <typeparamref name="TRight" /> value. If false, executes <paramref name="onFalse" /> to convert the filtered
/// <typeparamref name="TRight" /> to a <typeparamref name="TLeft" />.
/// </summary>
/// <remarks>
/// <para>
/// This overload of <see cref="Either{L,R}.Filter" /> avoids returning <see cref="Either{L,R}.Bottom" />.
/// </para>
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="filter">
/// A filtering function. If true, value continues as-is. If false, value is converted to a
/// <typeparamref name="TLeft" />.
/// </param>
/// <param name="onFalse">
/// A function to convert <typeparamref name="TRight" /> to a <typeparamref name="TLeft" />.
/// </param>
/// <returns>
/// If <paramref name="filter" /> returns true, current <typeparamref name="TRight" /> is returned. If it returns
/// false, the converted <typeparamref name="TLeft" /> is returned.
/// </returns>
public static Task<Either<TLeft, TRight>> FilterAsync<TLeft, TRight>(this Either<TLeft, TRight> either,
Func<TRight, Task<bool>> filter,
Func<TRight, TLeft> onFalse
)
{
return either.BindAsync(async right =>
await filter(right)
? Prelude.Right<TLeft, TRight>(right)
: Prelude.Left<TLeft, TRight>(value: onFalse(right)));
}

/// <summary>
/// Execute <paramref name="filter" />. If true, returns the current <typeparamref name="TRight" /> value. If false,
/// executes <paramref name="onFalse" /> to convert the filtered <typeparamref name="TRight" /> to a
/// <typeparamref name="TLeft" />.
/// </summary>
/// <remarks>
/// <para>
/// This overload of <see cref="Either{L,R}.Filter" /> avoids returning <see cref="Either{L,R}.Bottom" />.
/// </para>
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="filter">
/// A filtering function. If true, value continues as-is. If false, value is converted to a
/// <typeparamref name="TLeft" />.
/// </param>
/// <param name="onFalse">
/// A function to convert <typeparamref name="TRight" /> to a <typeparamref name="TLeft" />.
/// </param>
/// <returns>
/// If <paramref name="filter" /> returns true, current <typeparamref name="TRight" /> is returned. If it returns
/// false, the converted <typeparamref name="TLeft" /> is returned.
/// </returns>
public static Task<Either<TLeft, TRight>> FilterAsync<TLeft, TRight>(this Either<TLeft, TRight> either,
Func<TRight, Task<bool>> filter,
Func<TRight, Task<TLeft>> onFalse
)
{
return either.BindAsync(async right =>
await filter(right)
? Prelude.Right<TLeft, TRight>(right)
: Prelude.Left<TLeft, TRight>(value: await onFalse(right)));
}

/// <summary>
/// Execute a side effect and returns <paramref name="either" /> unchanged.
/// </summary>
/// <remarks>
/// Often used to perform logging.
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="onRight">A side-effect for right cases.</param>
/// <param name="onLeft">A side-effect for left cases.</param>
/// <returns>
/// <paramref name="either" />
/// </returns>
public static Either<TLeft, TRight> Tap<TLeft, TRight>(this Either<TLeft, TRight> either, Action<TRight> onRight,
Action<TLeft> onLeft)
{
return either.Match(right =>
{
onRight(right);
return either;
}, left =>
{
onLeft(left);
return either;
});
}

/// <summary>
/// Execute an asynchronous side effect and returns <paramref name="either" /> unchanged.
/// </summary>
/// <remarks>
/// Often used to dispatch an asynchronous status message, e.g., a workflow heartbeat.
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="onSuccess">A side-effect for Right cases.</param>
/// <param name="onFailure">A side-effect for Left cases.</param>
/// <returns>
/// <paramref name="either" />
/// </returns>
public static Task<Either<TLeft, TRight>> TapAsync<TLeft, TRight>(this Either<TLeft, TRight> either,
Func<TRight, Task> onSuccess,
Func<TLeft, Task> onFailure
)
{
return either.Match(async right =>
{
await onSuccess(right);
return either;
}, async left =>
{
await onFailure(left);
return either;
});
}

/// <summary>
/// Execute a side effect when <paramref name="either" /> is a Left and returns
/// <paramref name="either" /> unchanged.
/// </summary>
/// <remarks>
/// Often used to perform logging.
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="onFailure">A side-effect for Left cases.</param>
/// <returns>
/// <paramref name="either" />
/// </returns>
public static Either<TLeft, TRight> TapLeft<TLeft, TRight>(this Either<TLeft, TRight> either,
Action<TLeft> onFailure)
{
return either.Match(right => either, left =>
{
onFailure(left);
return either;
});
}

/// <summary>
/// Execute an asynchronous side effect when <paramref name="either" /> is a Left and returns
/// <paramref name="either" /> unchanged.
/// </summary>
/// <remarks>
/// Often used to dispatch an asynchronous status message, e.g., a workflow heartbeat.
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="onFailure">A side-effect for Left cases.</param>
/// <returns>
/// <paramref name="either" />
/// </returns>
public static Task<Either<TLeft, TRight>> TapLeftAsync<TLeft, TRight>(this Either<TLeft, TRight> either,
Func<TLeft, Task> onFailure)
{
return either.Match(right => either.AsTask(), async left =>
{
await onFailure(left);
return either;
});
}

/// <summary>
/// Execute a side effect when <paramref name="either" /> is a Right and returns <paramref name="either" /> unchanged.
/// </summary>
/// <remarks>
/// Often used to perform logging.
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="onSuccess">A side-effect for Right cases.</param>
/// <returns>
/// <paramref name="either" />
/// </returns>
public static Either<TLeft, TRight> TapRight<TLeft, TRight>(this Either<TLeft, TRight> either,
Action<TRight> onSuccess)
{
return either.Match(right =>
{
onSuccess(right);
return either;
}, _ => either);
}

/// <summary>
/// Execute an asynchronous side effect when <paramref name="either" /> is a Right and returns
/// <paramref name="either" /> unchanged.
/// </summary>
/// <remarks>
/// Often used to dispatch an asynchronous status message, e.g., a workflow heartbeat.
/// </remarks>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="onSuccess">A side-effect for Right cases.</param>
/// <returns>
/// <paramref name="either" />
/// </returns>
public static Task<Either<TLeft, TRight>> TapRightAsync<TLeft, TRight>(this Either<TLeft, TRight> either,
Func<TRight, Task> onSuccess
)
{
return either.Match(async right =>
{
await onSuccess(right);
return either;
}, _ => either.AsTask());
}

/// <summary>
/// Convert <paramref name="either" /> to a <see cref="Result{A}" />.
/// </summary>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <param name="ifLeft">
/// A function which will convert <typeparamref name="TLeft" /> values into <see cref="Exception" />
/// values.
/// </param>
/// <returns>A <see cref="Result{A}" />.</returns>
public static Result<TRight> ToResult<TLeft, TRight>(this Either<TLeft, TRight> either, Func<TLeft, Exception> ifLeft)
{
return either.Match(right => new Result<TRight>(right), left => new Result<TRight>(e: ifLeft(left)));
}

/// <summary>
/// Convert <paramref name="either" /> to a <see cref="Result{A}" />.
/// </summary>
/// <typeparam name="TLeft">A left type.</typeparam>
/// <typeparam name="TRight">A right type.</typeparam>
/// <param name="either">An <see cref="Either{L,R}" />.</param>
/// <returns>A <see cref="Result{A}" />.</returns>
public static Result<TRight> ToResult<TLeft, TRight>(this Either<TLeft, TRight> either)
where TLeft : Exception
{
return either.Match(right => new Result<TRight>(right), left => new Result<TRight>(left));
}
}
58 changes: 58 additions & 0 deletions tests/unit/EitherExtensionsTests/FilterAsyncTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using LanguageExt;

using Xunit;

namespace Jds.LanguageExt.Extras.Tests.Unit.EitherExtensionsTests;

public class FilterAsyncTests
{
private static async Task<bool> FilterFunc(int value)
{
await Task.Delay(1);
return value % 2 == 0;
}

private static string RightToLeftConverter(int value)
{
return value.ToString();
}

private static async Task<string> RightToLeftConverterAsync(int value)
{
await Task.Delay(1);
return value.ToString();
}

public static IEnumerable<object[]> CreateTestCases()
{
var goodValue = 42;
var badValue = 41;
return new[]
{
new object[] { Prelude.Right<string, int>(goodValue), Prelude.Right<string, int>(goodValue) },
new object[] { Prelude.Right<string, int>(badValue), Prelude.Left<string, int>(value: badValue.ToString()) },
new object[]
{
Prelude.Left<string, int>(value: "existing value"), Prelude.Left<string, int>(value: "existing value")
}
};
}

[Theory]
[MemberData(memberName: nameof(CreateTestCases))]
public async Task GivenSyncConverter_ReturnsExpectedResults(Either<string, int> either, Either<string, int> expected)
{
var actual = await either.FilterAsync(FilterFunc, RightToLeftConverter);

Assert.Equal(expected, actual);
}

[Theory]
[MemberData(memberName: nameof(CreateTestCases))]
public async Task GivenAsyncConverter_ReturnsExpectedResults(Either<string, int> either, Either<string, int> expected)
{
var actual = await either.FilterAsync(FilterFunc, RightToLeftConverterAsync);

Assert.Equal(expected, actual);
}
}
Loading