Skip to content
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
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/DotMaybe/MaybeNullableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ public static class MaybeReferenceTypeExtensions
/// <param name="maybe">The maybe.</param>
/// <returns>The value, or <see langword="null"/>.</returns>
public static T? ToNullable<T>(this Maybe<T> maybe)
where T : class => maybe.Match<T?>(v => v, () => null);
where T : class => maybe.TryGetSome(out var value) ? value : null;
}
37 changes: 29 additions & 8 deletions src/DotMaybe/MaybeTaskQueryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using static DotMaybe.Prelude;

namespace DotMaybe;

/// <summary>
Expand Down Expand Up @@ -98,8 +100,17 @@ public static ValueTask<Maybe<TResult>> SelectMany<T, TIntermediate, TResult>(
{
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<Maybe<TResult>>(none);

static async ValueTask<Maybe<TResult>> Project(
T value,
Func<T, Task<Maybe<TIntermediate>>> selector,
Func<T, TIntermediate, TResult> resultSelector) =>
(await selector(value).ConfigureAwait(false)).TryGetSome(out var selected)
? resultSelector(value, selected)
: none;
}

/// <summary>
Expand All @@ -124,7 +135,7 @@ public static ValueTask<Maybe<TResult>> SelectMany<T, TIntermediate, TResult>(
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(resultSelector);
return source.BindAsync(v1 => selector(v1).Map(v2 => resultSelector(v1, v2)));
return new ValueTask<Maybe<T>>(source).SelectMany(selector, resultSelector);
}

/// <summary>
Expand All @@ -149,8 +160,7 @@ public static ValueTask<Maybe<TResult>> SelectMany<T, TIntermediate, TResult>(
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<Maybe<T>>(source).SelectMany(selector, resultSelector);
}

/// <inheritdoc cref="SelectMany{T, TIntermediate, TResult}(Task{Maybe{T}}, Func{T, Maybe{TIntermediate}}, Func{T, TIntermediate, TResult})"/>
Expand All @@ -164,7 +174,13 @@ public static ValueTask<Maybe<TResult>> SelectMany<T, TIntermediate, TResult>(
{
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<Maybe<TResult>> SelectManyAfter(
ValueTask<Maybe<T>> source,
Func<T, Maybe<TIntermediate>> selector,
Func<T, TIntermediate, TResult> resultSelector) =>
(await source.ConfigureAwait(false)).SelectMany(selector, resultSelector);
}

/// <inheritdoc cref="SelectMany{T, TIntermediate, TResult}(Task{Maybe{T}}, Func{T, Task{Maybe{TIntermediate}}}, Func{T, TIntermediate, TResult})"/>
Expand All @@ -178,7 +194,12 @@ public static ValueTask<Maybe<TResult>> SelectMany<T, TIntermediate, TResult>(
{
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<Maybe<TResult>> SelectManyAfter(
ValueTask<Maybe<T>> source,
Func<T, Task<Maybe<TIntermediate>>> selector,
Func<T, TIntermediate, TResult> resultSelector) =>
await (await source.ConfigureAwait(false)).SelectMany(selector, resultSelector).ConfigureAwait(false);
}
}
7 changes: 1 addition & 6 deletions tests/DotMaybe.CompilerContractTests/Snippet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static class Snippet
{
private static readonly Lazy<ImmutableArray<MetadataReference>> 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)
Expand Down Expand Up @@ -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<MetadataReference> LoadReferences()
Expand Down
19 changes: 19 additions & 0 deletions tests/DotMaybe.PropertyTests/AllocationProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public void Reading_allocates_nothing(Maybe<int> 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)]
Expand Down Expand Up @@ -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<int> 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<T>(Maybe<T> maybe)
where T : notnull => maybe is T;

private static T ValueOr<T>(Maybe<T> maybe, T fallback)
where T : notnull => maybe switch
{
T value => value,
None => fallback,
};
}
2 changes: 1 addition & 1 deletion tests/DotMaybe.PropertyTests/MaybeArbitraries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static Arbitrary<Maybe<T>> MaybeOf<T>(Arbitrary<T> values)

private static IEnumerable<Maybe<T>> Shrink<T>(Maybe<T> maybe, Arbitrary<T> 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<T>())
Expand Down
2 changes: 1 addition & 1 deletion tests/DotMaybe.PropertyTests/MaybeAssert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ internal static class MaybeAssert
public static void HoldsValue<T>(T expected, Maybe<T> actual)
where T : notnull
{
if (Maybes.TryGetValue(actual, out var value))
if (actual is T value)
{
Assert.Equal(expected, value);
return;
Expand Down
15 changes: 3 additions & 12 deletions tests/DotMaybe.PropertyTests/Maybes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using static DotMaybe.Prelude;

namespace DotMaybe.PropertyTests;
Expand All @@ -8,9 +7,8 @@ namespace DotMaybe.PropertyTests;
/// </summary>
/// <remarks>
/// Building goes through the union conversions, which are never ambiguous in generic code
/// (e.g. for <c>Maybe&lt;Maybe&lt;int&gt;&gt;</c>). Reading goes through the union members. It predates the .NET 11
/// GA compiler, which accepts <c>maybe is T value</c> 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 <c>Maybe&lt;Maybe&lt;int&gt;&gt;</c>). Reading uses pattern matching, <c>maybe is T value</c>, which works
/// for a type parameter too (see the compiler contracts).
/// </remarks>
internal static class Maybes
{
Expand All @@ -20,17 +18,10 @@ public static Maybe<T> Some<T>(T value)
public static Maybe<T> Empty<T>()
where T : notnull => none;

public static bool TryGetValue<T>(Maybe<T> maybe, [MaybeNullWhen(false)] out T value)
where T : notnull
{
Maybe<T>.IUnionMembers members = maybe;
return members.TryGetValue(out value);
}

/// <summary>
/// Describes a maybe independently of <c>ToString</c>, for assertion messages.
/// </summary>
public static string Describe<T>(Maybe<T> maybe)
where T : notnull =>
TryGetValue(maybe, out var value) ? $"a value ({value})" : "None";
maybe is T value ? $"a value ({value})" : "None";
}
Loading