diff --git a/Railway/Error.cs b/Railway/Error.cs index 523da7d..fa723e7 100644 --- a/Railway/Error.cs +++ b/Railway/Error.cs @@ -52,7 +52,7 @@ public abstract class Error : IEquatable, IComparable [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static Error Many(Error error1, Error error2) => (error1, error2) switch { - (null, null) => new ManyErrors(new List()), + (null, null) => new ManyErrors(ImmutableArray.Empty), (Error err, null) => err, (Error err, { IsEmpty: true }) => err, (null, Error err) => err, @@ -137,12 +137,14 @@ public abstract class Error : IEquatable, IComparable return string.Compare(Message, other.Message); } - [Pure] public override string ToString() => Message; + [Pure] public sealed override string ToString() => Message; [Pure] public void Deconstruct(out string type, out string message) { type = Type; message = Message; } + + [Pure] internal virtual Error AccessUnsafe(int position) => this; } [JsonConverter(typeof(ExpectedErrorJsonConverter))] @@ -221,7 +223,7 @@ public sealed class ExceptionalError : Error if (!(exception.Data?.Count > 0)) return ImmutableDictionary.Empty; - List>? values = null; + ImmutableDictionary.Builder? values = null; foreach (var key in exception.Data.Keys) { @@ -234,63 +236,70 @@ public sealed class ExceptionalError : Error var valueString = value.ToString(); if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(valueString)) continue; - values ??= new List>(4); - values.Add(new(keyString, valueString)); + values ??= ImmutableDictionary.CreateBuilder(); + values.Add(keyString, valueString); } - return values?.ToImmutableDictionary() ?? ImmutableDictionary.Empty; + return values is not null ? values.ToImmutable() : ImmutableDictionary.Empty; } } [JsonConverter(typeof(ManyErrorsJsonConverter))] public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList { - private readonly List _errors; + private readonly ImmutableArray _errors; [Pure] public IEnumerable Errors { get => _errors; } - internal ManyErrors(List errors) => _errors = errors; + internal ManyErrors(ImmutableArray errors) => _errors = errors; internal ManyErrors(Error head, Error tail) { - _errors = new List(head.Count + tail.Count); + var headCount = head.Count; + var tailCount = tail.Count; + var errors = ImmutableArray.CreateBuilder(headCount + tailCount); - if (head.Count == 1) - _errors.Add(head); - else if (head.Count > 1) - _errors.AddRange(head.ToEnumerable()); + if (headCount > 0) + AppendSanitized(errors, head); - if (tail.Count == 1) - _errors.Add(tail); - else if (tail.Count > 1) - _errors.AddRange(tail.ToEnumerable()); + if (tailCount > 0) + AppendSanitized(errors, tail); + + _errors = errors.MoveToImmutable(); } public ManyErrors(IEnumerable errors) { - _errors = errors.SelectMany(x => x.ToEnumerable()) - .Where(x => !x.IsEmpty) - .ToList(); + var unpackedErrors = ImmutableArray.CreateBuilder(); + + foreach (var err in errors) + { + if (err.IsEmpty) continue; + + AppendSanitized(unpackedErrors, err); + } + + _errors = unpackedErrors.ToImmutable(); } [Pure] public override string Type => "many_errors"; - [Pure] public override string Message => ToFullArrayString(); - [Pure] public override string ToString() => ToFullArrayString(); - [Pure] private string ToFullArrayString() + private string? _lazyMessage = null; + [Pure] public override string Message => _lazyMessage ??= ToFullArrayString(_errors); + + [Pure] private static string ToFullArrayString(in ImmutableArray errors) { var separator = Environment.NewLine; - var lastIndex = _errors.Count - 1; var sb = new StringBuilder(); - for (int i = 0; i < _errors.Count; i++) + for (int i = 0; i < errors.Length; i++) { - sb.Append(_errors[i]); - if (i < lastIndex) - sb.Append(separator); + sb.Append(errors[i]); + sb.Append(separator); } + sb.Remove(sb.Length - separator.Length, separator.Length); return sb.ToString(); } - [Pure] public override int Count => _errors.Count; - [Pure] public override bool IsEmpty => _errors.Count == 0; + [Pure] public override int Count => _errors.Length; + [Pure] public override bool IsEmpty => _errors.IsEmpty; [Pure] public override bool IsExpected => _errors.All(static x => x.IsExpected); [Pure] public override bool IsExeptional => _errors.Any(static x => x.IsExeptional); @@ -303,22 +312,19 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList { if (other is null) return -1; - if (other.Count != _errors.Count) - return _errors.Count.CompareTo(other.Count); - - var compareResult = 0; - int i = 0; - foreach (var otherErr in other.ToEnumerable()) + if (other.Count != _errors.Length) + return _errors.Length.CompareTo(other.Count); + + for (int i = 0; i < _errors.Length; i++) { - var thisErr = _errors[i++]; - compareResult = thisErr.CompareTo(otherErr); + var compareResult = _errors[i].CompareTo(other.AccessUnsafe(i)); if (compareResult != 0) { return compareResult; } } - return compareResult; + return 0; } [Pure] public override bool IsSimilarTo([NotNullWhen(true)] Error? other) { @@ -326,15 +332,13 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList { return false; } - if (_errors.Count != other.Count) + if (_errors.Length != other.Count) { return false; } - int i = 0; - foreach (var otherErr in other.ToEnumerable()) + for (int i = 0; i < _errors.Length; i++) { - var thisErr = _errors[i++]; - if (!thisErr.IsSimilarTo(otherErr)) + if (!_errors[i].IsSimilarTo(other.AccessUnsafe(i))) { return false; } @@ -347,36 +351,49 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList { return false; } - if (_errors.Count != other.Count) + if (_errors.Length != other.Count) { return false; } - int i = 0; - foreach (var otherErr in other.ToEnumerable()) + for (int i = 0; i < _errors.Length; i++) { - var thisErr = _errors[i++]; - if (!thisErr.Equals(otherErr)) + if (!_errors[i].Equals(other.AccessUnsafe(i))) { return false; } } return true; } - [Pure] public override int GetHashCode() + + private int? _lazyHashCode = null; + [Pure] public override int GetHashCode() => _lazyHashCode ??= CalcHashCode(_errors); + private static int CalcHashCode(in ImmutableArray errors) { - if (_errors.Count == 0) + if (errors.IsEmpty) return 0; - + var hash = new HashCode(); - foreach (var err in _errors) + foreach (var err in errors) { hash.Add(err); } return hash.ToHashCode(); } - [Pure] public IEnumerator GetEnumerator() => _errors.GetEnumerator(); - [Pure] IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + [Pure] public ImmutableArray.Enumerator GetEnumerator() => _errors.GetEnumerator(); + [Pure] IEnumerator IEnumerable.GetEnumerator() => Errors.GetEnumerator(); + [Pure] IEnumerator IEnumerable.GetEnumerator() => Errors.GetEnumerator(); + + internal static void AppendSanitized(ImmutableArray.Builder errors, Error error) + { + if (error is ManyErrors many) + errors.AddRange(many._errors); + else + errors.Add(error); + } + + [Pure] internal override Error AccessUnsafe(int position) => _errors[position]; } [Serializable] diff --git a/Railway/ErrorJsonConverter.cs b/Railway/ErrorJsonConverter.cs index b6d8b27..5665875 100644 --- a/Railway/ErrorJsonConverter.cs +++ b/Railway/ErrorJsonConverter.cs @@ -35,7 +35,7 @@ public sealed class ErrorJsonConverter : JsonConverter internal static ManyErrors ReadMany(ref Utf8JsonReader reader) { - List errors = new(4); + var errors = ImmutableArray.CreateBuilder(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.StartObject) @@ -43,7 +43,7 @@ public sealed class ErrorJsonConverter : JsonConverter errors.Add(ToExpectedError(ReadOne(ref reader))); } } - return new ManyErrors(errors); + return new ManyErrors(errors.ToImmutable()); } internal static ExpectedError ToExpectedError(in (string Type, string Message, ImmutableDictionary ExtensionData) errorInfo) => new(errorInfo.Type, errorInfo.Message) { ExtensionData = errorInfo.ExtensionData }; diff --git a/Railway/ResultExtensions.cs b/Railway/ResultExtensions.cs index 9a41900..468a1cb 100644 --- a/Railway/ResultExtensions.cs +++ b/Railway/ResultExtensions.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; + namespace Just.Railway; public static partial class ResultExtensions @@ -80,7 +82,7 @@ public static partial class ResultExtensions public static Result Merge(this IEnumerable results) { - List? errors = null; + ImmutableArray.Builder? errors = null; bool hasErrors = false; foreach (var result in results.OrderBy(x => x.State)) @@ -89,8 +91,8 @@ public static partial class ResultExtensions { case ResultState.Error: hasErrors = true; - errors ??= new(4); - errors.Add(result.Error!); + errors ??= ImmutableArray.CreateBuilder(); + ManyErrors.AppendSanitized(errors, result.Error!); break; case ResultState.Success: @@ -102,7 +104,7 @@ public static partial class ResultExtensions } afterLoop: return hasErrors - ? new(new ManyErrors(errors!)) + ? new(new ManyErrors(errors!.ToImmutable())) : new(null); } public static async Task Merge(this IEnumerable> tasks) @@ -113,8 +115,8 @@ public static partial class ResultExtensions public static Result> Merge(this IEnumerable> results) { - List? values = null; - List? errors = null; + ImmutableList.Builder? values = null; + ImmutableArray.Builder? errors = null; bool hasErrors = false; foreach (var result in results.OrderBy(x => x.State)) @@ -123,13 +125,13 @@ public static partial class ResultExtensions { case ResultState.Error: hasErrors = true; - errors ??= new(4); - errors.Add(result.Error!); + errors ??= ImmutableArray.CreateBuilder(); + ManyErrors.AppendSanitized(errors, result.Error!); break; case ResultState.Success: if (hasErrors) goto afterLoop; - values ??= new(4); + values ??= ImmutableList.CreateBuilder(); values.Add(result.Value); break; @@ -138,8 +140,8 @@ public static partial class ResultExtensions } afterLoop: return hasErrors - ? new(new ManyErrors(errors!)) - : new((IEnumerable?)values ?? Array.Empty()); + ? new(new ManyErrors(errors!.ToImmutable())) + : new(values is not null ? values.ToImmutable() : ImmutableList.Empty); } public static async Task>> Merge(this IEnumerable>> tasks) {