diff --git "a/Documentation/src/analyzer-rules/\316\2731005.md" "b/Documentation/src/analyzer-rules/\316\2731005.md" index 67eec4e27..03533eca4 100644 --- "a/Documentation/src/analyzer-rules/\316\2731005.md" +++ "b/Documentation/src/analyzer-rules/\316\2731005.md" @@ -24,7 +24,7 @@ static int Example(Option option) => option.Match(none: () => 42, some: Identity); static int Example(Result result) - => result.Match(ok: x => x, error: _ => 42); + => result.Match(error: _ => 42, ok: x => x); ``` ### Allowed diff --git "a/Documentation/src/analyzer-rules/\316\2731006.md" "b/Documentation/src/analyzer-rules/\316\2731006.md" index 2d5b73d19..1702db17d 100644 --- "a/Documentation/src/analyzer-rules/\316\2731006.md" +++ "b/Documentation/src/analyzer-rules/\316\2731006.md" @@ -23,7 +23,7 @@ static Option Example(Option option, Option fallback) => option.Match(none: () => fallback, some: x => Option.Some(x)); static Result Example(Result result, Result fallback) - => result.Match(ok: Result.Return, error: _ => fallback); + => result.Match(error: _ => fallback, ok: Result.Return); ``` ### Allowed diff --git a/Documentation/src/either.md b/Documentation/src/either.md index 2bd58e04e..15575260e 100644 --- a/Documentation/src/either.md +++ b/Documentation/src/either.md @@ -30,8 +30,8 @@ Every operation from [`Option`](./option.md) and `Result` exists on `Eithe |-----------------------|------------------------| | `Ok(value)` | `Right(value)` | | `Error(exception)` | `Left(value)` | -| `Match(ok, error)` | `Match(left, right)` | -| `Switch(ok, error)` | `Switch(left, right)` | +| `Match(error, ok)` | `Match(left, right)` | +| `Switch(error, ok)` | `Switch(left, right)` | | `InspectError` | `InspectLeft` | `Select`, `SelectMany`, `OrElse`, `GetOrElse` and `Inspect` work the same way, and so does query syntax. As with diff --git a/Documentation/src/fp/overview.md b/Documentation/src/fp/overview.md index 61441f50b..f6786f9ca 100644 --- a/Documentation/src/fp/overview.md +++ b/Documentation/src/fp/overview.md @@ -174,8 +174,8 @@ where the two tracks merge back into a plain value and the caller decides what t ```cs PlaceOrder(request).Match( - ok: order => Results.Created($"/orders/{order.Id}", order), - error: exception => Results.BadRequest(exception.Message)); + error: exception => Results.BadRequest(exception.Message), + ok: order => Results.Created($"/orders/{order.Id}", order)); ``` The one discipline the style asks for is to stay on the tracks until the end. Unwrapping a `Result` in the diff --git a/Documentation/src/result.md b/Documentation/src/result.md index 57a892841..8f0b9f691 100644 --- a/Documentation/src/result.md +++ b/Documentation/src/result.md @@ -19,8 +19,8 @@ difference is that the empty case is called `error` instead of `none`, and that |--------------------|--------------------------| | `Some(value)` | `Ok(value)` | | `None` | `Error(exception)` | -| `Match(none, some)`| `Match(ok, error)` | -| `Switch(none, some)`| `Switch(ok, error)` | +| `Match(none, some)`| `Match(error, ok)` | +| `Switch(none, some)`| `Switch(error, ok)` | | `InspectNone` | `InspectError` | | `ToNullable` | `GetOrThrow` | @@ -116,8 +116,8 @@ As with options, `Match` is the general tool and should be the last one you reac ```cs IActionResult response = PlaceOrder(customerId, productId, quantity).Match( - ok: order => Created(order), - error: exception => BadRequest(exception.Message)); + error: exception => BadRequest(exception.Message), + ok: order => Created(order)); ``` `Switch` is the same for actions. Always name the arguments; [λ1003](./analyzer-rules/λ1003.md) enforces it, and diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/AlternativeMonad/AlternativeMonadAnalyzerTest.Stubs.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/AlternativeMonad/AlternativeMonadAnalyzerTest.Stubs.cs index 718033935..c6dc6d735 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/AlternativeMonad/AlternativeMonadAnalyzerTest.Stubs.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/AlternativeMonad/AlternativeMonadAnalyzerTest.Stubs.cs @@ -97,7 +97,7 @@ public readonly struct Result public Result SelectMany(System.Func> selector) where TResult : notnull => default; - public TMatchResult Match(System.Func ok, System.Func error) => default!; + public TMatchResult Match(System.Func error, System.Func ok) => default!; } public static class Result diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/Fallback/FallbackStubs.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/Fallback/FallbackStubs.cs index 4bd8de49e..0ab1651da 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/Fallback/FallbackStubs.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/Fallback/FallbackStubs.cs @@ -65,7 +65,7 @@ public readonly struct Result public TValidResult GetOrElse(System.Func fallback) => default!; - public TMatchResult Match(System.Func ok, System.Func error) => default!; + public TMatchResult Match(System.Func error, System.Func ok) => default!; } } """; diff --git a/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadAnalyzer.cs index 13b7107fa..12c26a261 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadAnalyzer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadAnalyzer.cs @@ -45,14 +45,21 @@ private static Option MatchMatchInvocation(IInvocationOperation => invocation is { TargetMethod: { ReceiverType: INamedTypeSymbol receiverType, Name: MatchMethodName } } && alternativeMonadTypes.Value.TryGetValue(receiverType.ConstructedFrom, out var alternativeMonadType) && invocation.GetArgumentsInParameterOrder() is { Length: 2 } arguments - ? [new MatchInvocation( - receiverType, - alternativeMonadType, - arguments[alternativeMonadType.ErrorStateArgumentIndex], - arguments[alternativeMonadType.SuccessStateArgumentIndex]), - ] + && ErrorStateIndex(arguments, alternativeMonadType) is { } errorStateIndex + ? [new MatchInvocation(receiverType, alternativeMonadType, arguments[errorStateIndex], arguments[1 - errorStateIndex])] : []; + // The arguments are told apart by parameter name, so both the Funcky 3.x and the 4.0.0 order of Result.Match work. + private static int? ErrorStateIndex(ArgumentsInParameterOrder arguments, AlternativeMonadType alternativeMonadType) + { + if (arguments[0].Parameter?.Name == alternativeMonadType.ErrorStateParameterName) + { + return 0; + } + + return arguments[1].Parameter?.Name == alternativeMonadType.ErrorStateParameterName ? 1 : null; + } + private static Diagnostic? AnalyzeMatchInvocation( IInvocationOperation operation, MatchInvocation match) diff --git a/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadType.cs b/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadType.cs index e6bbc15c5..aedd825bf 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadType.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadType.cs @@ -8,7 +8,7 @@ internal sealed class AlternativeMonadType( INamedTypeSymbol constructorsType, string returnAlias, string errorStateConstructorName, - bool matchHasSuccessStateFirst = false, + string errorStateParameterName, INamedTypeSymbol? extensionsType = null) { private readonly Lazy _hasGetOrElse = HasMethodLazy(type, GetOrElseMethodName); @@ -23,9 +23,10 @@ internal sealed class AlternativeMonadType( public string ErrorStateConstructorName { get; } = errorStateConstructorName; - public int ErrorStateArgumentIndex => matchHasSuccessStateFirst ? 1 : 0; - - public int SuccessStateArgumentIndex => matchHasSuccessStateFirst ? 0 : 1; + /// The name of the Match parameter that handles the error state (none, left, error). The + /// arguments are told apart by name rather than by position, so the analyzer works with Funcky 3.x, where Result.Match + /// took the success state first, and with Funcky 4.0.0, where the error state comes first everywhere. + public string ErrorStateParameterName { get; } = errorStateParameterName; public bool HasGetOrElse => _hasGetOrElse.Value; diff --git a/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadTypeCollection.cs b/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadTypeCollection.cs index 39504a587..34181e2ff 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadTypeCollection.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/AlternativeMonad/AlternativeMonadTypeCollection.cs @@ -36,6 +36,7 @@ private static void AddAlternativeMonadType(IDictionaryThe Match parameters that handle the error state of each alternative monad. + public const string OptionMatchNoneParameterName = "none"; + + public const string EitherMatchLeftParameterName = "left"; + + public const string ResultMatchErrorParameterName = "error"; + public const string EitherRightMethodName = "Right"; public const string EitherLeftMethodName = "Left"; diff --git a/Funcky/Monads/Result/Result.Core.cs b/Funcky/Monads/Result/Result.Core.cs index f0dd90430..33b3fdfa8 100644 --- a/Funcky/Monads/Result/Result.Core.cs +++ b/Funcky/Monads/Result/Result.Core.cs @@ -95,7 +95,7 @@ public static Result Error(Exception exception) [Pure] [UseWithArgumentNames] - public TMatchResult Match(Func ok, Func error) + public TMatchResult Match(Func error, Func ok) => _state switch { State.Ok => ok(_result), @@ -105,7 +105,7 @@ public TMatchResult Match(Func ok, Fun }; [UseWithArgumentNames] - public void Switch(Action ok, Action error) + public void Switch(Action error, Action ok) { switch (_state) { @@ -135,20 +135,20 @@ public bool Equals(Result other) public override int GetHashCode() => Match( uninitialized: static () => 0, - ok: static result => result.GetHashCode(), - error: static error => error.GetHashCode()); + error: static error => error.GetHashCode(), + ok: static result => result.GetHashCode()); [Pure] public override string ToString() => Match( uninitialized: static () => "Uninitialized", - ok: static result => $"Ok({result})", - error: static exception => $"Error({exception.GetType().FullName}: {exception.Message})"); + error: static exception => $"Error({exception.GetType().FullName}: {exception.Message})", + ok: static result => $"Ok({result})"); /// A total match that also handles the invalid state of a default value; for members that must never throw. [Pure] [UseWithArgumentNames] - internal TMatchResult Match(Func uninitialized, Func ok, Func error) + internal TMatchResult Match(Func uninitialized, Func error, Func ok) => _state switch { State.Ok => ok(_result), diff --git a/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 66c503ce4..5f1654d83 100644 --- a/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -118,3 +118,7 @@ *REMOVED*static Funcky.Extensions.AsyncEnumerableExtensions.WhereSelectAwait(this System.Collections.Generic.IAsyncEnumerable! source, System.Func>>! selector) -> System.Collections.Generic.IAsyncEnumerable! *REMOVED*static Funcky.Extensions.AsyncEnumerableExtensions.WhereSelectAwaitWithCancellation(this System.Collections.Generic.IAsyncEnumerable! source, System.Func>>! selector) -> System.Collections.Generic.IAsyncEnumerable! *REMOVED*static Funcky.Extensions.AsyncEnumerableExtensions.WhereSelectAwaitWithCancellation(this System.Collections.Generic.IAsyncEnumerable! source, System.Func>>! selector) -> System.Collections.Generic.IAsyncEnumerable! +*REMOVED*Funcky.Monads.Result.Match(System.Func! ok, System.Func! error) -> TMatchResult +*REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void +Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult +Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void diff --git a/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt index e9fc8675c..7172b081f 100644 --- a/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt @@ -164,3 +164,7 @@ static Funcky.Extensions.StringExtensions.LastIndexOfOrNone(this string! haystac *REMOVED*static Funcky.Extensions.AsyncEnumerableExtensions.WhereSelectAwait(this System.Collections.Generic.IAsyncEnumerable! source, System.Func>>! selector) -> System.Collections.Generic.IAsyncEnumerable! *REMOVED*static Funcky.Extensions.AsyncEnumerableExtensions.WhereSelectAwaitWithCancellation(this System.Collections.Generic.IAsyncEnumerable! source, System.Func>>! selector) -> System.Collections.Generic.IAsyncEnumerable! *REMOVED*static Funcky.Extensions.AsyncEnumerableExtensions.WhereSelectAwaitWithCancellation(this System.Collections.Generic.IAsyncEnumerable! source, System.Func>>! selector) -> System.Collections.Generic.IAsyncEnumerable! +*REMOVED*Funcky.Monads.Result.Match(System.Func! ok, System.Func! error) -> TMatchResult +*REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void +Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult +Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void diff --git a/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt index e72aadc33..3c6842c20 100644 --- a/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -2,3 +2,7 @@ *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option +*REMOVED*Funcky.Monads.Result.Match(System.Func! ok, System.Func! error) -> TMatchResult +*REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void +Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult +Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void diff --git a/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt index e72aadc33..3c6842c20 100644 --- a/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -2,3 +2,7 @@ *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option +*REMOVED*Funcky.Monads.Result.Match(System.Func! ok, System.Func! error) -> TMatchResult +*REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void +Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult +Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void diff --git a/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index e72aadc33..3c6842c20 100644 --- a/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -2,3 +2,7 @@ *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option +*REMOVED*Funcky.Monads.Result.Match(System.Func! ok, System.Func! error) -> TMatchResult +*REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void +Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult +Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void diff --git a/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index e72aadc33..3c6842c20 100644 --- a/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -2,3 +2,7 @@ *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option *REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option +*REMOVED*Funcky.Monads.Result.Match(System.Func! ok, System.Func! error) -> TMatchResult +*REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void +Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult +Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void diff --git a/README.md b/README.md index 497c13504..4334fa53a 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,8 @@ Result order = ParseOrder(json) .SelectMany(Price); return order.Match( - ok: o => Results.Ok(o), - error: e => Results.BadRequest(e.Message)); + error: e => Results.BadRequest(e.Message), + ok: o => Results.Ok(o)); ``` ### IEnumerable extensions diff --git a/changelog.md b/changelog.md index 60aaea9f1..d68ecb003 100644 --- a/changelog.md +++ b/changelog.md @@ -33,6 +33,12 @@ Funcky adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). single-name overloads with a `(…, cancellationToken)` delegate. The code fix in Funcky.Analyzers 3.0.0 now also fires on the compile error CS1061 that a removed name leaves behind, so a project can upgrade first and let Fix-All rewrite every call afterwards; the fix for the obsoletion warning CS0618 on 3.8.0 stays as well. +* Breaking: `Result.Match` and `Result.Switch` take the error case first, `Match(error, ok)` and + `Switch(error, ok)`, consistent with `Option.Match(none, some)` and `Either.Match(left, right)`. Both methods carry + `[UseWithArgumentNames]`, so calls that follow λ1003 and name their arguments are unaffected; a positional call fails + to compile after the upgrade (the two delegates have different parameter types), swap its arguments. The `Match` + rules of Funcky.Analyzers (λ1005 to λ1007 and λ1011) now tell the two arguments apart by parameter name, so they work + with either order. * Funcky and Funcky.Async add a `net11.0` asset and are built with the .NET 11 SDK (release candidate 3 while .NET 11 is in preview). The `net11.0` asset of Funcky.Async only forwards to Funcky, like the `net10.0` one. No target framework is removed in this step; see `SupportPolicy.md` for the frameworks Funcky 4 drops.