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

With Extensions (Combine) #342

Open
wants to merge 1 commit into
base: master
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

namespace CSharpFunctionalExtensions.Tests.ResultTests.Extensions.With
{
public class AsyncResultOfTETests
{
[Fact]
public async Task Combining_successful()
{
var r1 = Result.Success<int, string>(1);
var r2 = Result.Success<int, string>(2);
var actual = await r1.WithMap(r2, (a, b) => Task.FromResult(a + b), (e1, e2) => "failure");

actual.IsSuccess.Should().BeTrue();
}

[Fact]
public async Task Successful_results_are_combined_with_binding()
{
var r1 = Result.Success<int, string>(1);
var r2 = Result.Success<int, string>(2);

var actual = await r1
.WithBind(r2, (a, b) => Task.FromResult(Result.Success<int, string>(a + b)), (e1, e2) => "error");

actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be(3);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

namespace CSharpFunctionalExtensions.Tests.ResultTests.Extensions.With
{
public class AsyncResultOfTTests
{
[Fact]
public async Task Successful_results_are_combined_with_mapping()
{
var r1 = Result.Success(1);
var r2 = Result.Success("hola");

var actual = await r1
.WithMap(r2, (a, b) => Task.FromResult(a + b));

actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be("1hola");
}

[Fact]
public async Task Successful_results_are_combined_with_binding()
{
var r1 = Result.Success(1);
var r2 = Result.Success("hola");

var actual = await r1
.WithBind(r2, (a, b) => Task.FromResult(Result.Success(a + b)));

actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be("1hola");
}

[Fact]
public async Task Successful_results_are_combined_into_success()
{
var r1 = Result.Success(1);
var r2 = Result.Success("hola");

var actual = await r1
.WithBind(r2, (a, b) => Task.FromResult(Result.Success()));

actual.IsSuccess.Should().BeTrue();
}

[Fact]
public async Task Failures_are_combined_into_failure()
{
var r1 = Result.Failure<int>("Failed");
var r2 = Result.Success("hola");

var actual = await r1
.WithBind(r2, (a, b) => Task.FromResult(Result.Success()));

actual.IsSuccess.Should().BeFalse();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using FluentAssertions;
using Xunit;

namespace CSharpFunctionalExtensions.Tests.ResultTests.Extensions.With
{
public class ResultOfTETests
{
[Fact]
public async void Successful_results_are_combined_with_map()
{
var r1 = Result.Success<int, string>(1);
var r2 = Result.Success<int, string>(2);

var actual = r1.WithMap(r2, (a, b) => a + b, (e1, e2) => "");

actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be(3);
}

[Fact]
public async void Successful_results_are_combined_with_binding()
{
var r1 = Result.Success<int, string>(1);
var r2 = Result.Success<int, string>(2);

var actual = r1.WithBind<int, string>(r2, (a, b) => Result.Success<int, string>(a + b), (e1, e2) => "");

actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be(3);
}

[Fact]
public async void Successful_results_are_combined_with_binding_different_types()
{
var r1 = Result.Success<string, string>("Hi");
var r2 = Result.Success<int, string>(2);

var actual = r1.WithBind(r2, (a, b) => Result.Success<string, string>(a + b), (e1, e2) => "");

actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be("Hi2");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using FluentAssertions;
using Xunit;

namespace CSharpFunctionalExtensions.Tests.ResultTests.Extensions.With
{
public class ResultOfTTest
{
[Fact]
public void Results_of_same_type_are_combined_correctly()
{
var r1 = Result.Success("Hello");
var r2 = Result.Success("world");

var actual = r1.With(r2);
actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be(("Hello", "world"));
}

[Fact]
public void Results_of_different_type_are_mapped_correctly()
{
var r1 = Result.Success("Hello");
var r2 = Result.Success("world");

var actual = r1.WithMap(r2, (a, b) => a + b);
actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be("Helloworld");
}

[Fact]
public void Results_of_different_type_are_bound_correctly()
{
var r1 = Result.Success("Hello");
var r2 = Result.Success("world");

var actual = r1.WithBind(r2, (a, b) => Result.Success(a + b));
actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be("Helloworld");
}
}
}
4 changes: 4 additions & 0 deletions CSharpFunctionalExtensions/CSharpFunctionalExtensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
<ItemGroup Condition="'$(TargetFramework)'=='net40'">
<PackageReference Include="Microsoft.Bcl.Async" Version="1.0.168" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net4')) OR $(TargetFramework)=='netstandard2.0'">
<PackageReference Include="System.ValueTuple" Version="4.*" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

System.ValueTuple is shipped with the legacy framework starting from 4.7 and with netstandard starting from 2.0.

Copy link
Contributor

@ProphetLamb ProphetLamb Jul 6, 2022

Choose a reason for hiding this comment

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

Comparing TargetFramework is considered leaky, for various good reasons, such as Mono RT, and "oh I missed a version", additionally we are not interested in the TargetFramework at all, but in its features.
Preprocessor directives express features.

Instead, consider comparing using $(DefineConstants.Contains('NET5_0_OR_GREATER')) which are supported by 3rd party runtimes and also well documented.

</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
Expand Down
29 changes: 29 additions & 0 deletions CSharpFunctionalExtensions/Result/Extensions/BindError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;

namespace CSharpFunctionalExtensions
{
public partial class ResultExtensions
{
public static Result<T, E2> BindError<T, E, E2>(this Result<T, E> self,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doesn't OnFailureCompensate cover such a case?

Copy link
Author

Choose a reason for hiding this comment

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

I don't think so. OnFailureCompensate expects the same error type :) This maps from E, to E2

Func<E, Result<T, E2>> func)
{
if (self.IsSuccess)
{
return Result.Success<T, E2>(self.Value);
}

return func(self.Error);
}

public static Result<T> BindError<T>(this Result<T> self,
Func<string, Result<T>> func)
{
if (self.IsSuccess)
{
return Result.Success(self.Value);
}

return func(self.Error);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Threading.Tasks;

namespace CSharpFunctionalExtensions
{
public static partial class ResultExtensions
{
public static Task<Result<TResult>> WithMap<T1, T2, TResult>(this Result<T1> a,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel it can be expressed via SelectMany

Copy link
Author

Choose a reason for hiding this comment

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

If you're able to give an example, I'll be good with that :D

Result<T2> b,
Func<T1, T2, Task<TResult>> func)
{
var mapSuccess =
a.BindError(e1 => b
.MapError(e2 => Errors.Join(e1, e2))
.Bind(_ => Result.Failure<T1>(e1)))
.Bind(x => b
.Map(y => func(x, y))
.MapError(el => el));

return mapSuccess;
}

public static Task<Result<TResult>> WithBind<T1, T2, TResult>(this Result<T1> a,
Result<T2> b,
Func<T1, T2, Task<Result<TResult>>> func)
{
var mapSuccess =
a.BindError(e1 => b
.MapError(e2 => Errors.Join(e1, e2))
.Bind(_ => Result.Failure<T1>(e1)))
.Bind(x => b
.Bind(y => func(x, y))
.MapError(el => el));

return mapSuccess;
}

public static Task<Result> WithBind<T1, T2>(this Result<T1> a,
Result<T2> b,
Func<T1, T2, Task<Result>> map)
{
var mapSuccess =
a.BindError(el1 => b
.MapError(el2 => string.Join(Result.ErrorMessagesSeparator, el1, el2))
Copy link
Contributor

Choose a reason for hiding this comment

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

Use Errors.Join

.Bind(_ => Result.Failure<T1>(el1)))
.Bind(x => b
.Bind(y => map(x, y))
.MapError(el => el));

return mapSuccess;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Threading.Tasks;

namespace CSharpFunctionalExtensions
{
public static partial class ResultExtensions
{
public static Task<Result<TResult, E>> WithMap<T1, T2, E, TResult>(this Result<T1, E> a,
Result<T2, E> b,
Func<T1, T2, Task<TResult>> map, Func<E, E, E> combineError)
{
var mapSuccess =
a.BindError(e1 => b
.MapError(e2 => combineError(e1, e2))
.Bind(_ => Result.Failure<T1, E>(e1)))
.Bind(x => b
.Map(y => map(x, y))
.MapError(el => el));

return mapSuccess;
}

public static Task<Result<TResult, E>> WithBind<T1, T2, E, TResult>(this Result<T1, E> a,
Result<T2, E> b,
Func<T1, T2, Task<Result<TResult, E>>> map, Func<E, E, E> combineError)
{
var mapSuccess =
a.BindError(e1 => b
.MapError(e2 => combineError(e1, e2))
.Bind(_ => Result.Failure<T1, E>(e1)))
.Bind(x => b
.Bind(y => map(x, y))
.MapError(el => el));

return mapSuccess;
}
}
}
10 changes: 10 additions & 0 deletions CSharpFunctionalExtensions/Result/Extensions/With/Errors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace CSharpFunctionalExtensions
{
public static class Errors
{
public static string Join(string e1, string e2)
{
return string.Join(Result.ErrorMessagesSeparator, e1, e2);
Copy link
Contributor

@ProphetLamb ProphetLamb Jul 6, 2022

Choose a reason for hiding this comment

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

Use string interpolation $"{e1}{Result.ErrorMessagesSeparator}{e2}"

Copy link
Contributor

Choose a reason for hiding this comment

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

Would be sensible to wait for #378 with the error combining

}
}
}
40 changes: 40 additions & 0 deletions CSharpFunctionalExtensions/Result/Extensions/With/ResultOfT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;

namespace CSharpFunctionalExtensions
{
public static partial class ResultExtensions
{
public static Result<(T1, T2)> With<T1, T2>(this Result<T1> a, Result<T2> b)
Copy link
Contributor

Choose a reason for hiding this comment

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

Declare argument covariance in this & in for every non TAP, non closure usages accepting structs.

{
return a.WithMap(b, (x, y) => (x, y));
Copy link
Contributor

Choose a reason for hiding this comment

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

Use static () {} lambda in all instances where no closure allocation is required.

}

public static Result<TResult> WithMap<T, K, TResult>(
this Result<T> a,
Result<K> b,
Func<T, K, TResult> func)
{
return a
.BindError(e1 => b
.MapError(e2 => Errors.Join(e1, e2))
.Bind(_ => Result.Failure<T>(e1))
).Bind(x => b
.Map(y => func(x, y))
.MapError(e => e));
}

public static Result<TResult> WithBind<T, K, TResult>(
this Result<T> a,
Result<K> b,
Func<T, K, Result<TResult>> func)
{
return a
.BindError(e1 => b
.MapError(e2 => Errors.Join(e1, e2))
.Bind(_ => Result.Failure<T>(e1))
).Bind(x => b
.Bind(y => func(x, y))
.MapError(e => e));
}
}
}
Loading