Skip to content

Commit 9792199

Browse files
authored
Improved Linq integration (#32)
add WhereSuccess/WhereError rename FirstOrError to TryFirst
1 parent 054ddfe commit 9792199

File tree

7 files changed

+235
-10
lines changed

7 files changed

+235
-10
lines changed

benchy/Program.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using Ametrin.Optional;
2-
using Ametrin.Optional.Benchy;
3-
using Ametrin.Optional.Benchy.Examples;
1+
using Ametrin.Optional.Benchy;
42
using BenchmarkDotNet.Running;
53

64
BenchmarkRunner.Run<TestBenchmarks>();

benchy/TestBenchmarks.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,51 @@ namespace Ametrin.Optional.Benchy;
55
[MemoryDiagnoser(false)]
66
public class TestBenchmarks
77
{
8+
[Params(.1f, .5f)]
9+
public float ErrorRate;
810

11+
[Params(500)]
12+
public int Count;
13+
14+
private Result<int>[] array = [];
15+
private List<Result<int>> list = [];
16+
17+
[GlobalSetup]
18+
public void Setup()
19+
{
20+
var random = new Random(69);
21+
array = [.. Enumerable.Range(0, Count).Select(_ => random.NextSingle() > ErrorRate ? Result.Success(random.Next()) : Result.Error<int>())];
22+
list = [.. array];
23+
}
24+
25+
[Benchmark]
26+
public void Split_Array()
27+
{
28+
var r = array.Branch();
29+
}
30+
31+
[Benchmark]
32+
public void Split_List()
33+
{
34+
var r = list.Branch();
35+
}
936
}
37+
38+
// | Method | ErrorRate | Count | Mean | Error | StdDev | Median | Allocated |
39+
// |------------ |---------- |------ |-----------:|---------:|----------:|-----------:|----------:|
40+
// | Split_Array | 0 | 100 | 134.6 ns | 0.95 ns | 0.89 ns | 134.6 ns | 624 B |
41+
// | Split_List | 0 | 100 | 581.5 ns | 3.32 ns | 2.94 ns | 580.5 ns | 640 B |
42+
// | Split_Array | 0 | 1000 | 1,299.7 ns | 6.21 ns | 5.81 ns | 1,297.8 ns | 4944 B |
43+
// | Split_List | 0 | 1000 | 5,632.6 ns | 23.29 ns | 21.79 ns | 5,634.6 ns | 4960 B |
44+
// | Split_Array | 0.1 | 100 | 169.7 ns | 1.31 ns | 1.16 ns | 169.7 ns | 808 B |
45+
// | Split_List | 0.1 | 100 | 606.3 ns | 2.76 ns | 2.58 ns | 606.2 ns | 824 B |
46+
// | Split_Array | 0.1 | 1000 | 1,462.7 ns | 11.68 ns | 10.35 ns | 1,464.1 ns | 6568 B |
47+
// | Split_List | 0.1 | 1000 | 5,819.1 ns | 24.95 ns | 22.12 ns | 5,811.4 ns | 6584 B |
48+
// | Split_Array | 0.2 | 100 | 168.3 ns | 0.98 ns | 0.91 ns | 168.3 ns | 808 B |
49+
// | Split_List | 0.2 | 100 | 607.0 ns | 2.07 ns | 1.83 ns | 606.8 ns | 824 B |
50+
// | Split_Array | 0.2 | 1000 | 1,507.0 ns | 24.71 ns | 23.12 ns | 1,505.8 ns | 6568 B |
51+
// | Split_List | 0.2 | 1000 | 5,905.5 ns | 19.03 ns | 17.80 ns | 5,903.0 ns | 6584 B |
52+
// | Split_Array | 0.5 | 100 | 229.5 ns | 0.73 ns | 0.68 ns | 229.4 ns | 1816 B |
53+
// | Split_List | 0.5 | 100 | 671.7 ns | 1.76 ns | 1.65 ns | 672.1 ns | 1832 B |
54+
// | Split_Array | 0.5 | 1000 | 2,081.3 ns | 72.14 ns | 210.42 ns | 1,992.3 ns | 16216 B |
55+
// | Split_List | 0.5 | 1000 | 6,421.6 ns | 28.57 ns | 25.32 ns | 6,420.8 ns | 16232 B |

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- fixed missing default values in generated async extension methods
1515
- warning for empty `Consume` calls (`AmOptional009`)
1616
- improvements to Wrong conditional return type (AmOptional003) analyzer
17+
- (TUnit) `IsSuccess(condition)` for `Option<T>` `Result<T>` and `Result<T, E>`
1718
- (TUnit) `IsError(condition)` for `Result<T>`, `Result<T, E>`, `ErrorState` and `ErrorState<E>`
1819
- updated TUnit to 1.0.30
1920

samples/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,4 @@ static void NullableIntegrationExample()
126126
?? "No value"; // Provide default
127127

128128
Console.WriteLine($"Nullable result: {result}");
129-
}
129+
}

src/Extensions/OptionLinqExtensions.cs

Lines changed: 162 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,181 @@ namespace Ametrin.Optional;
55

66
public static class OptionLinqExtensions
77
{
8+
// any already tries to get the non-enumerated count before calling the enumerator
89
public static Option<IEnumerable<T>> RejectEmpty<T>(this IEnumerable<T> source)
910
=> source is not null && source.Any() ? Option.Success(source) : default;
1011
public static Option<IEnumerable<T>> RejectEmpty<T>(this Option<IEnumerable<T>> option)
11-
=> option.Require(static collection => collection.Any());
12+
=> option.Require(Enumerable.Any);
1213
public static Result<IEnumerable<T>> RejectEmpty<T>(this Result<IEnumerable<T>> option)
13-
=> option.RejectEmpty(static value => new ArgumentException("Sequence was empty"));
14+
=> option.RejectEmpty(static source => new ArgumentException("Sequence was empty"));
1415
public static Result<IEnumerable<T>> RejectEmpty<T>(this Result<IEnumerable<T>> option, Func<IEnumerable<T>, Exception> error)
15-
=> option.Require(static collection => collection.Any(), error);
16+
=> option.Require(Enumerable.Any, error);
17+
public static Result<IEnumerable<T>, E> RejectEmpty<T, E>(this Result<IEnumerable<T>, E> option, E error)
18+
=> option.Require(Enumerable.Any, error);
19+
public static Result<IEnumerable<T>, E> RejectEmpty<T, E>(this Result<IEnumerable<T>, E> option, Func<IEnumerable<T>, E> error)
20+
=> option.Require(Enumerable.Any, error);
1621

17-
public static Option<T> FirstOrNone<T>(this IEnumerable<T> source)
22+
public static Option<T> TryFirst<T>(this IEnumerable<T> source)
1823
{
1924
using var enumerator = source.GetEnumerator();
2025
return enumerator.MoveNext() ? Option.Success(enumerator.Current) : default;
2126
}
2227

2328
public static IEnumerable<T> WhereSuccess<T>(this IEnumerable<Option<T>> source)
2429
=> source.Where(static option => option._hasValue).Select(static option => option._value);
30+
public static IEnumerable<T> WhereSuccess<T>(this IEnumerable<Result<T>> source)
31+
=> source.Where(static option => option._hasValue).Select(static option => option._value);
32+
public static IEnumerable<T> WhereSuccess<T, E>(this IEnumerable<Result<T, E>> source)
33+
=> source.Where(static option => option._hasValue).Select(static option => option._value);
34+
35+
public static IEnumerable<Exception> WhereError<T>(this IEnumerable<Result<T>> source)
36+
=> source.Where(static option => !option._hasValue).Select(static option => option._error);
37+
public static IEnumerable<E> WhereError<T, E>(this IEnumerable<Result<T, E>> source)
38+
=> source.Where(static option => !option._hasValue).Select(static option => option._error);
39+
public static IEnumerable<Exception> WhereError<T>(this IEnumerable<ErrorState> source)
40+
=> source.Where(static option => option._isError).Select(static option => option._error);
41+
public static IEnumerable<E> WhereError<T, E>(this IEnumerable<ErrorState<E>> source)
42+
=> source.Where(static option => option._isError).Select(static option => option._error);
43+
44+
public static Option<IReadOnlyList<T>> ValuesOrError<T>(this IEnumerable<Option<T>> source)
45+
{
46+
var count = source.TryGetNonEnumeratedCount(out var c) ? c : -1;
47+
if (count is 0) return Option.Success<IReadOnlyList<T>>([]);
48+
var values = CreateBag<T>(count);
49+
50+
foreach (var result in source)
51+
{
52+
if (result.Branch(out var value))
53+
{
54+
values.Add(value);
55+
}
56+
else
57+
{
58+
values.Clear();
59+
return default;
60+
}
61+
}
62+
63+
return values;
64+
}
65+
66+
public static Result<IReadOnlyList<T>> ValuesOrFirstError<T>(this IEnumerable<Result<T>> source)
67+
{
68+
var count = source.TryGetNonEnumeratedCount(out var c) ? c : -1;
69+
if (count is 0) return Result.Success<IReadOnlyList<T>>([]);
70+
var values = CreateBag<T>(count);
71+
72+
foreach (var result in source)
73+
{
74+
if (result.Branch(out var value, out var error))
75+
{
76+
values.Add(value);
77+
}
78+
else
79+
{
80+
values.Clear();
81+
return error;
82+
}
83+
}
84+
85+
return values;
86+
}
87+
88+
public static Result<IReadOnlyList<T>, E> ValuesOrFirstError<T, E>(this IEnumerable<Result<T, E>> source)
89+
{
90+
var count = source.TryGetNonEnumeratedCount(out var c) ? c : -1;
91+
if (count is 0) return Result.Success<IReadOnlyList<T>, E>([]);
92+
var values = CreateBag<T>(count);
93+
94+
foreach (var result in source)
95+
{
96+
if (result.Branch(out var value, out var error))
97+
{
98+
values.Add(value);
99+
}
100+
else
101+
{
102+
values.Clear();
103+
return error;
104+
}
105+
}
106+
107+
return values;
108+
}
109+
110+
public static void BranchInto<T>(this IEnumerable<Result<T>> results, IList<T> values, IList<Exception> errors)
111+
{
112+
foreach (var result in results)
113+
{
114+
if (result.Branch(out var value, out var error))
115+
{
116+
values.Add(value);
117+
}
118+
else
119+
{
120+
errors.Add(error);
121+
}
122+
}
123+
}
124+
125+
public static void BranchInto<T, E>(this IEnumerable<Result<T, E>> results, IList<T> values, IList<E> errors)
126+
{
127+
foreach (var result in results)
128+
{
129+
if (result.Branch(out var value, out var error))
130+
{
131+
values.Add(value);
132+
}
133+
else
134+
{
135+
errors.Add(error);
136+
}
137+
}
138+
}
139+
140+
public static (IReadOnlyList<T> values, IReadOnlyList<Exception> errors) Branch<T>(this IEnumerable<Result<T>> results)
141+
{
142+
ArgumentNullException.ThrowIfNull(results);
143+
144+
var count = results.TryGetNonEnumeratedCount(out var c) ? c : -1;
145+
if (count is 0) return ([], []);
146+
147+
var (values, errors) = CreateBags<T, Exception>(count);
148+
results.BranchInto(values, errors);
149+
150+
return (values, errors);
151+
}
152+
153+
public static (IReadOnlyList<T> values, IReadOnlyList<E> errors) Branch<T, E>(this IEnumerable<Result<T, E>> results)
154+
{
155+
ArgumentNullException.ThrowIfNull(results);
156+
157+
var count = results.TryGetNonEnumeratedCount(out var c) ? c : -1;
158+
if (count is 0) return ([], []);
159+
160+
var (values, errors) = CreateBags<T, E>(count);
161+
results.BranchInto(values, errors);
162+
163+
return (values, errors);
164+
}
165+
166+
private static (List<T> values, List<E> errors) CreateBags<T, E>(int count, double expectedErrorRate = 0.01)
167+
{
168+
// we assume most of the incoming values will be successes so we preallocate the full size
169+
var values = count > 0 ? new List<T>(capacity: count) : [];
170+
// in most cases only a fraction of values will be errors (this has not been benchmarked yet! first me must figure out what a common error rate is)
171+
var errors = count > 0 ? new List<E>(capacity: (int)double.Round(count * expectedErrorRate)) : [];
172+
173+
return (values, errors);
174+
}
175+
176+
private static List<T> CreateBag<T>(int count)
177+
{
178+
// we assume most of the incoming values will be successes so we preallocate the full size
179+
return count > 0 ? new List<T>(capacity: count) : [];
180+
}
181+
182+
[Obsolete("use Select(option => option.Map(map)). this method made things confusing")]
25183
public static IEnumerable<Option<TResult>> Select<T, TResult>(this IEnumerable<Option<T>> source, Func<T, TResult> map)
26184
=> source.Select(option => option.Map(map));
27185
}

testing/TUnit/OptionAssertionExtensions.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.ComponentModel;
3-
using System.Runtime.CompilerServices;
44
using TUnit.Assertions.Attributes;
55
using TUnit.Assertions.Conditions;
66
using TUnit.Assertions.Core;
@@ -13,7 +13,14 @@ public static class OptionAssertionExtensions
1313
[GenerateAssertion(ExpectationMessage = "to be {expected}")]
1414
public static bool IsSuccess<TValue>(this Option<TValue> option, TValue expected)
1515
{
16-
return option.Branch(out var value) ? EqualityComparer<TValue>.Default.Equals(value, expected) : false;
16+
return option.Branch(out var value) && EqualityComparer<TValue>.Default.Equals(value, expected);
17+
}
18+
19+
[EditorBrowsable(EditorBrowsableState.Never)]
20+
[GenerateAssertion(ExpectationMessage = ErrorStateAssertionExtensions.EXPECTED_SUCCESS_MESSAGE)]
21+
public static bool IsSuccess<TValue>(this Option<TValue> option, Func<TValue, bool> condition)
22+
{
23+
return option.Branch(out var value) && condition(value);
1724
}
1825

1926
[EditorBrowsable(EditorBrowsableState.Never)]

testing/TUnit/ResultAssertionExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ public static bool IsSuccess<TValue>(this Result<TValue> result, TValue expected
1919
return result.Branch(out var value, out _) && EqualityComparer<TValue>.Default.Equals(value, expected);
2020
}
2121

22+
[EditorBrowsable(EditorBrowsableState.Never)]
23+
[GenerateAssertion(ExpectationMessage = ErrorStateAssertionExtensions.EXPECTED_SUCCESS_MESSAGE)]
24+
public static bool IsSuccess<TValue>(this Result<TValue> result, Func<TValue, bool> condition)
25+
{
26+
return result.Branch(out var value, out _) && condition(value);
27+
}
28+
2229
[EditorBrowsable(EditorBrowsableState.Never)]
2330
[GenerateAssertion(ExpectationMessage = ErrorStateAssertionExtensions.EXPECTED_SUCCESS_MESSAGE)]
2431
public static bool IsSuccess<TValue>(this Result<TValue> result)
@@ -72,6 +79,14 @@ public static bool IsSuccess<TValue, TError>(this Result<TValue, TError> result,
7279
return result.Branch(out var value, out _) && EqualityComparer<TValue>.Default.Equals(value, expected);
7380
}
7481

82+
[EditorBrowsable(EditorBrowsableState.Never)]
83+
[GenerateAssertion(ExpectationMessage = ErrorStateAssertionExtensions.EXPECTED_SUCCESS_MESSAGE)]
84+
public static bool IsSuccess<TValue, TError>(this Result<TValue, TError> result, Func<TValue, bool> condition)
85+
{
86+
return result.Map(condition).Or(false);
87+
}
88+
89+
7590
/// <summary>
7691
/// Asserts the <see cref="Result{TValue, TError}"/> is Error with a specific value
7792
/// </summary>

0 commit comments

Comments
 (0)