diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index af34433..bb50c45 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -92,12 +92,13 @@ on every call); read the fields or use `TryGetSome` instead.
Benchmarks are for measuring by hand, not part of CI (CI only builds them):
```bash
-dotnet run -c Release --project benchmarks/DotMaybe.Benchmarks -- --filter '*'
+dotnet run -c Release --project benchmarks/DotMaybe.Benchmarks -- --filter '*' --inProcess
dotnet run -c Release --project benchmarks/DotMaybe.Benchmarks -- --filter '*Chain*' --inProcess
```
-`--inProcess` runs without generating a separate project, which helps while BenchmarkDotNet does not know a new
-.NET version yet.
+`--inProcess` runs without generating a separate project. It is required while BenchmarkDotNet does not know the
+.NET version: BenchmarkDotNet 0.15.8 fails on .NET 11 without it (`GetRuntimeVersion not implemented for
+NotRecognized`). Drop it once a BenchmarkDotNet release supports .NET 11.
## Quality checks
diff --git a/src/DotMaybe/MaybeNullableExtensions.cs b/src/DotMaybe/MaybeNullableExtensions.cs
index f84e2d5..88f5a5a 100644
--- a/src/DotMaybe/MaybeNullableExtensions.cs
+++ b/src/DotMaybe/MaybeNullableExtensions.cs
@@ -31,5 +31,5 @@ public static class MaybeReferenceTypeExtensions
/// The maybe.
/// The value, or .
public static T? ToNullable(this Maybe maybe)
- where T : class => maybe.Match(v => v, () => null);
+ where T : class => maybe.TryGetSome(out var value) ? value : null;
}
diff --git a/src/DotMaybe/MaybeTaskQueryExtensions.cs b/src/DotMaybe/MaybeTaskQueryExtensions.cs
index 0dba2ba..a096720 100644
--- a/src/DotMaybe/MaybeTaskQueryExtensions.cs
+++ b/src/DotMaybe/MaybeTaskQueryExtensions.cs
@@ -1,3 +1,5 @@
+using static DotMaybe.Prelude;
+
namespace DotMaybe;
///
@@ -98,8 +100,17 @@ public static ValueTask> SelectMany(
{
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(resultSelector);
- return source.BindAsync(async v1 =>
- (await selector(v1).ConfigureAwait(false)).Map(v2 => resultSelector(v1, v2)));
+ return source.TryGetSome(out var value)
+ ? Project(value, selector, resultSelector)
+ : ValueTask.FromResult>(none);
+
+ static async ValueTask> Project(
+ T value,
+ Func>> selector,
+ Func resultSelector) =>
+ (await selector(value).ConfigureAwait(false)).TryGetSome(out var selected)
+ ? resultSelector(value, selected)
+ : none;
}
///
@@ -124,7 +135,7 @@ public static ValueTask> SelectMany(
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(resultSelector);
- return source.BindAsync(v1 => selector(v1).Map(v2 => resultSelector(v1, v2)));
+ return new ValueTask>(source).SelectMany(selector, resultSelector);
}
///
@@ -149,8 +160,7 @@ public static ValueTask> SelectMany(
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(resultSelector);
- return source.BindAsync(async v1 =>
- (await selector(v1).ConfigureAwait(false)).Map(v2 => resultSelector(v1, v2)));
+ return new ValueTask>(source).SelectMany(selector, resultSelector);
}
///
@@ -164,7 +174,13 @@ public static ValueTask> SelectMany(
{
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(resultSelector);
- return source.BindAsync(v1 => selector(v1).Map(v2 => resultSelector(v1, v2)));
+ return SelectManyAfter(source, selector, resultSelector);
+
+ static async ValueTask> SelectManyAfter(
+ ValueTask> source,
+ Func> selector,
+ Func resultSelector) =>
+ (await source.ConfigureAwait(false)).SelectMany(selector, resultSelector);
}
///
@@ -178,7 +194,12 @@ public static ValueTask> SelectMany(
{
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(resultSelector);
- return source.BindAsync(async v1 =>
- (await selector(v1).ConfigureAwait(false)).Map(v2 => resultSelector(v1, v2)));
+ return SelectManyAfter(source, selector, resultSelector);
+
+ static async ValueTask> SelectManyAfter(
+ ValueTask> source,
+ Func>> selector,
+ Func resultSelector) =>
+ await (await source.ConfigureAwait(false)).SelectMany(selector, resultSelector).ConfigureAwait(false);
}
}
diff --git a/tests/DotMaybe.CompilerContractTests/Snippet.cs b/tests/DotMaybe.CompilerContractTests/Snippet.cs
index 47ec3b8..27a319c 100644
--- a/tests/DotMaybe.CompilerContractTests/Snippet.cs
+++ b/tests/DotMaybe.CompilerContractTests/Snippet.cs
@@ -12,7 +12,7 @@ internal static class Snippet
{
private static readonly Lazy> References = new(LoadReferences);
- private static readonly CSharpParseOptions ParseOptions = new(CSharp15());
+ private static readonly CSharpParseOptions ParseOptions = new(LanguageVersion.CSharp15);
private static readonly CSharpCompilationOptions CompilationOptions =
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
@@ -43,11 +43,6 @@ public static CompilationResult Compile(string source)
return new CompilationResult(source, diagnostics);
}
- private static LanguageVersion CSharp15() =>
- LanguageVersionFacts.TryParse("15", out var version)
- ? version
- : throw new InvalidOperationException("The SDK's compiler does not know C# 15. Check global.json.");
-
// The running test host already loads the .NET 11 framework and DotMaybe, so its trusted platform
// assemblies are exactly what a consumer compiles against.
private static ImmutableArray LoadReferences()
diff --git a/tests/DotMaybe.PropertyTests/AllocationProperties.cs b/tests/DotMaybe.PropertyTests/AllocationProperties.cs
index 99cf7b9..e3eb400 100644
--- a/tests/DotMaybe.PropertyTests/AllocationProperties.cs
+++ b/tests/DotMaybe.PropertyTests/AllocationProperties.cs
@@ -25,6 +25,8 @@ public void Reading_allocates_nothing(Maybe maybe)
Allocations.AssertNone("Tap", () => _ = maybe.Tap(static _ => { }));
Allocations.AssertNone("ToNullable", () => _ = maybe.ToNullable());
Allocations.AssertNone("switch", () => _ = maybe switch { int value => value, None => 0 });
+ Allocations.AssertNone("is T (generic)", () => _ = IsSome(maybe));
+ Allocations.AssertNone("switch (generic)", () => _ = ValueOr(maybe, 0));
}
[Property(Skip = Allocations.SkipInDebug)]
@@ -124,7 +126,24 @@ public void Async_steps_on_completed_sources_with_synchronous_steps_allocate_not
Allocations.AssertNone(
"async query",
() => _ = (from value in fromValueTask where value > 0 select value + 1).IsCompleted);
+ Allocations.AssertNone(
+ "async query with two from (ValueTask)",
+ () => _ = (from value in fromValueTask from half in Half(value) select value + half).IsCompleted);
+ Allocations.AssertNone(
+ "async query with two from (Task)",
+ () => _ = (from value in fromTask from half in Half(value) select value + half).IsCompleted);
}
private static Maybe Half(int value) => value % 2 == 0 ? value / 2 : none;
+
+ // Pattern matching through a type parameter, as consumers write it in generic code since the .NET 11 GA compiler.
+ private static bool IsSome(Maybe maybe)
+ where T : notnull => maybe is T;
+
+ private static T ValueOr(Maybe maybe, T fallback)
+ where T : notnull => maybe switch
+ {
+ T value => value,
+ None => fallback,
+ };
}
diff --git a/tests/DotMaybe.PropertyTests/MaybeArbitraries.cs b/tests/DotMaybe.PropertyTests/MaybeArbitraries.cs
index 05e1446..86c2cfd 100644
--- a/tests/DotMaybe.PropertyTests/MaybeArbitraries.cs
+++ b/tests/DotMaybe.PropertyTests/MaybeArbitraries.cs
@@ -27,7 +27,7 @@ public static Arbitrary> MaybeOf(Arbitrary values)
private static IEnumerable> Shrink(Maybe maybe, Arbitrary values)
where T : notnull =>
- Maybes.TryGetValue(maybe, out var value)
+ maybe is T value
? values.Shrinker(value)
.Select(smaller => Maybes.Some(smaller))
.Prepend(Maybes.Empty())
diff --git a/tests/DotMaybe.PropertyTests/MaybeAssert.cs b/tests/DotMaybe.PropertyTests/MaybeAssert.cs
index 347e904..a58f9ec 100644
--- a/tests/DotMaybe.PropertyTests/MaybeAssert.cs
+++ b/tests/DotMaybe.PropertyTests/MaybeAssert.cs
@@ -8,7 +8,7 @@ internal static class MaybeAssert
public static void HoldsValue(T expected, Maybe actual)
where T : notnull
{
- if (Maybes.TryGetValue(actual, out var value))
+ if (actual is T value)
{
Assert.Equal(expected, value);
return;
diff --git a/tests/DotMaybe.PropertyTests/Maybes.cs b/tests/DotMaybe.PropertyTests/Maybes.cs
index ab69682..d2ecc05 100644
--- a/tests/DotMaybe.PropertyTests/Maybes.cs
+++ b/tests/DotMaybe.PropertyTests/Maybes.cs
@@ -1,4 +1,3 @@
-using System.Diagnostics.CodeAnalysis;
using static DotMaybe.Prelude;
namespace DotMaybe.PropertyTests;
@@ -8,9 +7,8 @@ namespace DotMaybe.PropertyTests;
///
///
/// Building goes through the union conversions, which are never ambiguous in generic code
-/// (e.g. for Maybe<Maybe<int>>). Reading goes through the union members. It predates the .NET 11
-/// GA compiler, which accepts maybe is T value for a type parameter (see the compiler contracts); the RC1
-/// compiler rejected it with CS8780. Tests on concrete types read values with pattern matching.
+/// (e.g. for Maybe<Maybe<int>>). Reading uses pattern matching, maybe is T value, which works
+/// for a type parameter too (see the compiler contracts).
///
internal static class Maybes
{
@@ -20,17 +18,10 @@ public static Maybe Some(T value)
public static Maybe Empty()
where T : notnull => none;
- public static bool TryGetValue(Maybe maybe, [MaybeNullWhen(false)] out T value)
- where T : notnull
- {
- Maybe.IUnionMembers members = maybe;
- return members.TryGetValue(out value);
- }
-
///
/// Describes a maybe independently of ToString, for assertion messages.
///
public static string Describe(Maybe maybe)
where T : notnull =>
- TryGetValue(maybe, out var value) ? $"a value ({value})" : "None";
+ maybe is T value ? $"a value ({value})" : "None";
}