diff --git a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/InspectEmptyTest.cs b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/InspectEmptyTest.cs new file mode 100644 index 00000000..7a95357c --- /dev/null +++ b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/InspectEmptyTest.cs @@ -0,0 +1,35 @@ +using Funcky.Async.Test.TestUtilities; + +namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions; + +public sealed class InspectEmptyTest +{ + [Fact] + public void InspectEmptyIsEnumeratedLazily() + { + var doNotEnumerate = new FailOnEnumerateAsyncSequence(); + _ = doNotEnumerate.InspectEmpty(NoOperation); + } + + [Fact] + public async Task InspectEmptyExecutesAnInspectionFunctionOnMaterializationOnAnEmptyEnumerable() + { + var sideEffect = 0; + var asyncEnumerable = AsyncEnumerable.Empty(); + + _ = await asyncEnumerable.InspectEmpty(() => sideEffect = 1).MaterializeAsync(); + + Assert.Equal(1, sideEffect); + } + + [Fact] + public void InspectEmptyExecutesNoInspectionFunctionOnMaterializationOnANonEmptyEnumerable() + { + var sideEffect = 0; + var asyncEnumerable = AsyncSequence.Return("Hello", "World"); + + _ = asyncEnumerable.InspectEmpty(() => sideEffect = 1).MaterializeAsync(); + + Assert.Equal(0, sideEffect); + } +} diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/InspectEmpty.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/InspectEmpty.cs new file mode 100644 index 00000000..c43befeb --- /dev/null +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/InspectEmpty.cs @@ -0,0 +1,41 @@ +using System.Runtime.CompilerServices; + +namespace Funcky.Extensions; + +public static partial class AsyncEnumerableExtensions +{ + /// + /// An IAsyncEnumerable that calls a function if and only if the source has no element to enumerate. It can be used to encode side effects on an empty IAsyncEnumerable. + /// The side effect will be executed when enumerating the result. + /// + /// the inner type of the enumerable. + /// returns an with the side effect defined by action encoded in the async enumerable. + [Pure] + public static IAsyncEnumerable InspectEmpty(this IAsyncEnumerable source, Action inspector) + => InspectEmptyInternal(source, inspector); + + private static async IAsyncEnumerable InspectEmptyInternal( + this IAsyncEnumerable source, + Action inspector, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using var enumerator = source.ConfigureAwait(false).WithCancellation(cancellationToken).GetAsyncEnumerator(); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + + if (await enumerator.MoveNextAsync()) + { + yield return enumerator.Current; + } + else + { + inspector(); + yield break; + } + + while (await enumerator.MoveNextAsync()) + { + yield return enumerator.Current; + } + } +} diff --git a/Funcky.Async/PublicAPI.Unshipped.txt b/Funcky.Async/PublicAPI.Unshipped.txt index c2ca6bce..ec67fe26 100644 --- a/Funcky.Async/PublicAPI.Unshipped.txt +++ b/Funcky.Async/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable +static Funcky.Extensions.AsyncEnumerableExtensions.InspectEmpty(this System.Collections.Generic.IAsyncEnumerable! source, System.Action! inspector) -> System.Collections.Generic.IAsyncEnumerable! static Funcky.Monads.OptionAsyncExtensions.ToAsyncEnumerable(this Funcky.Monads.Option option) -> System.Collections.Generic.IAsyncEnumerable! diff --git a/Funcky.Test/Extensions/EnumerableExtensions/InspectEmptyTest.cs b/Funcky.Test/Extensions/EnumerableExtensions/InspectEmptyTest.cs new file mode 100644 index 00000000..4087fcfe --- /dev/null +++ b/Funcky.Test/Extensions/EnumerableExtensions/InspectEmptyTest.cs @@ -0,0 +1,36 @@ +#pragma warning disable SA1010 // StyleCop support for collection expressions is missing +using Funcky.Test.TestUtils; + +namespace Funcky.Test.Extensions.EnumerableExtensions; + +public sealed class InspectEmptyTest +{ + [Fact] + public void InspectEmptyIsEnumeratedLazily() + { + var doNotEnumerate = new FailOnEnumerationSequence(); + _ = doNotEnumerate.InspectEmpty(NoOperation); + } + + [Fact] + public void InspectEmptyExecutesAnInspectionFunctionOnMaterializationOnAnEmptyEnumerable() + { + var sideEffect = 0; + IEnumerable enumerable = []; + + _ = enumerable.InspectEmpty(() => sideEffect = 1).Materialize(); + + Assert.Equal(1, sideEffect); + } + + [Fact] + public void InspectEmptyExecutesNoInspectionFunctionOnMaterializationOnANonEmptyEnumerable() + { + var sideEffect = 0; + IEnumerable enumerable = ["Hello", "World"]; + + _ = enumerable.InspectEmpty(() => sideEffect = 1).Materialize(); + + Assert.Equal(0, sideEffect); + } +} diff --git a/Funcky/Extensions/EnumerableExtensions/InspectEmpty.cs b/Funcky/Extensions/EnumerableExtensions/InspectEmpty.cs new file mode 100644 index 00000000..664728c4 --- /dev/null +++ b/Funcky/Extensions/EnumerableExtensions/InspectEmpty.cs @@ -0,0 +1,31 @@ +namespace Funcky.Extensions; + +public static partial class EnumerableExtensions +{ + /// + /// An IEnumerable that calls a function if and only if the source has no element to enumerate. It can be used to encode side effects on an empty IEnumerable. + /// The side effect will be executed when enumerating the result. + /// + /// the inner type of the enumerable. + /// returns an with the side effect defined by action encoded in the enumerable. + [Pure] + public static IEnumerable InspectEmpty(this IEnumerable source, Action inspector) + { + using var enumerator = source.GetEnumerator(); + + if (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + else + { + inspector(); + yield break; + } + + while (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + } +} diff --git a/Funcky/PublicAPI.Unshipped.txt b/Funcky/PublicAPI.Unshipped.txt index f452730f..c3d89b86 100644 --- a/Funcky/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI.Unshipped.txt @@ -10,6 +10,7 @@ Funcky.Monads.Result.InspectError(System.Action Funcky.Monads.Result.OrElse(Funcky.Monads.Result fallback) -> Funcky.Monads.Result Funcky.Monads.Result.OrElse(System.Func>! fallback) -> Funcky.Monads.Result Funcky.UpCast +static Funcky.Extensions.EnumerableExtensions.InspectEmpty(this System.Collections.Generic.IEnumerable! source, System.Action! inspector) -> System.Collections.Generic.IEnumerable! static Funcky.Extensions.EnumeratorExtensions.MoveNextOrNone(this System.Collections.Generic.IEnumerator! enumerator) -> Funcky.Monads.Option static Funcky.Extensions.ParseExtensions.ParseByteOrNone(this System.ReadOnlySpan candidate, System.Globalization.NumberStyles style, System.IFormatProvider? provider) -> Funcky.Monads.Option static Funcky.Extensions.ParseExtensions.ParseByteOrNone(this System.ReadOnlySpan candidate, System.IFormatProvider? provider) -> Funcky.Monads.Option