From df7afb83610693870037300443405ec54ac7f654 Mon Sep 17 00:00:00 2001 From: David R Date: Tue, 5 Mar 2024 08:52:41 +0100 Subject: [PATCH] Feature/option as struct (#37) * Change option to readonly struct * Update major version to 6 * - adapt naming --------- Co-authored-by: Alexander Wiedemann --- .../MyError.cs | 2 +- Source/FunicularSwitch/FunicularSwitch.csproj | 4 +- Source/FunicularSwitch/Option.cs | 111 ++++++------------ .../FunicularSwitch.Test/ImplicitCastStudy.cs | 21 ++-- .../Tests/FunicularSwitch.Test/OptionSpecs.cs | 2 +- 5 files changed, 50 insertions(+), 90 deletions(-) diff --git a/Source/FunicularSwitch.Generators.Templates/MyError.cs b/Source/FunicularSwitch.Generators.Templates/MyError.cs index f36a64f..141e352 100644 --- a/Source/FunicularSwitch.Generators.Templates/MyError.cs +++ b/Source/FunicularSwitch.Generators.Templates/MyError.cs @@ -68,7 +68,7 @@ internal enum UnionCases public override string ToString() => Enum.GetName(typeof(UnionCases), UnionCase) ?? UnionCase.ToString(); bool Equals(MyError other) => UnionCase == other.UnionCase; - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; diff --git a/Source/FunicularSwitch/FunicularSwitch.csproj b/Source/FunicularSwitch/FunicularSwitch.csproj index fc7391e..cc08864 100644 --- a/Source/FunicularSwitch/FunicularSwitch.csproj +++ b/Source/FunicularSwitch/FunicularSwitch.csproj @@ -13,8 +13,8 @@ - 5 - 1.1 + 6 + 0.0 $(MajorVersion).0.0 diff --git a/Source/FunicularSwitch/Option.cs b/Source/FunicularSwitch/Option.cs index 729d7c3..829f849 100644 --- a/Source/FunicularSwitch/Option.cs +++ b/Source/FunicularSwitch/Option.cs @@ -7,31 +7,41 @@ namespace FunicularSwitch { - public abstract class Option + public static class Option { - public static Option Some(T value) => new Some(value); + public static Option Some(T value) => Option.Some(value); public static Option None() => Option.None; public static async Task> Some(Task value) => Some(await value); public static Task> NoneAsync() => Task.FromResult(Option.None); } - public abstract class Option : Option, IEnumerable + public readonly struct Option : IEnumerable { -#pragma warning disable CS0109 // Member does not hide an inherited member; new keyword is not required - public new static readonly Option None = new None(); -#pragma warning restore CS0109 // Member does not hide an inherited member; new keyword is not required + public static readonly Option None = default; - public bool IsSome() => GetType() == typeof(Some); + public static Option Some(T value) => new(value); - public bool IsNone() => !IsSome(); + readonly bool _isSome; - public Option Map(Func map) => Match(t => Some(map(t)), None); + readonly T _value; - public Task> Map(Func> map) => Match(async t => Some(await map(t).ConfigureAwait(false)), () => Task.FromResult(None())); + Option(T value) + { + _isSome = true; + _value = value; + } + + public bool IsSome() => _isSome; + + public bool IsNone() => !_isSome; - public Option Bind(Func> map) => Match(map, None); + public Option Map(Func map) => Match(t => Option.Some(map(t)), Option.None); - public Task> Bind(Func>> bind) => Match(bind, () => None()); + public Task> Map(Func> map) => Match(async t => Option.Some(await map(t).ConfigureAwait(false)), () => Task.FromResult(Option.None)); + + public Option Bind(Func> map) => Match(map, Option.None); + + public Task> Bind(Func>> bind) => Match(bind, () => Option.None); public void Match(Action some, Action? none = null) { @@ -40,10 +50,9 @@ public void Match(Action some, Action? none = null) public async Task Match(Func some, Func? none = null) { - var iAmSome = this as Some; - if (iAmSome != null) + if (_isSome) { - await some(iAmSome.Value).ConfigureAwait(false); + await some(_value).ConfigureAwait(false); } else if (none != null) { @@ -51,24 +60,15 @@ public async Task Match(Func some, Func? none = null) } } - public TResult Match(Func some, Func none) - { - var iAmSome = this as Some; - return iAmSome != null ? some(iAmSome.Value) : none(); - } + public TResult Match(Func some, Func none) => _isSome ? some(_value) : none(); - public TResult Match(Func some, TResult none) - { - var iAmSome = this as Some; - return iAmSome != null ? some(iAmSome.Value) : none; - } + public TResult Match(Func some, TResult none) => _isSome ? some(_value) : none; public async Task Match(Func> some, Func> none) { - var iAmSome = this as Some; - if (iAmSome != null) + if (_isSome) { - return await some(iAmSome.Value).ConfigureAwait(false); + return await some(_value).ConfigureAwait(false); } return await none().ConfigureAwait(false); @@ -76,10 +76,9 @@ public async Task Match(Func> some, Func Match(Func> some, Func none) { - var iAmSome = this as Some; - if (iAmSome != null) + if (_isSome) { - return await some(iAmSome.Value).ConfigureAwait(false); + return await some(_value).ConfigureAwait(false); } return none(); @@ -87,10 +86,9 @@ public async Task Match(Func> some, Func Match(Func> some, TResult none) { - var iAmSome = this as Some; - if (iAmSome != null) + if (_isSome) { - return await some(iAmSome.Value).ConfigureAwait(false); + return await some(_value).ConfigureAwait(false); } return none; @@ -104,54 +102,15 @@ public async Task Match(Func> some, TResult n public T? GetValueOrDefault() => Match(v => (T?)v, () => default); - public T GetValueOrDefault(Func defaultValue) => Match(v => v, () => defaultValue()); + public T GetValueOrDefault(Func defaultValue) => Match(v => v, defaultValue); public T GetValueOrDefault(T defaultValue) => Match(v => v, () => defaultValue); public T GetValueOrThrow(string? errorMessage = null) => Match(v => v, () => throw new InvalidOperationException(errorMessage ?? "Cannot access value of none option")); - public Option Convert() => Match(s => Some((TOther)(object)s!), None); - - public override string ToString() => Match(v => v?.ToString() ?? "", () => $"None {GetType().BeautifulName()}"); - } - - public sealed class Some : Option - { - public T Value { get; } - - public Some(T value) => Value = value; - - bool Equals(Some other) => EqualityComparer.Default.Equals(Value, other.Value); - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((Some)obj); - } - - public override int GetHashCode() => EqualityComparer.Default.GetHashCode(Value); - - public static bool operator ==(Some? left, Some? right) => Equals(left, right); - - public static bool operator !=(Some? left, Some? right) => !Equals(left, right); - } - - public sealed class None : Option - { - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType(); - } - - public override int GetHashCode() => typeof(None).GetHashCode(); - - public static bool operator ==(None left, None right) => Equals(left, right); + public Option Convert() => Match(s => Option.Some((TOther)(object)s!), Option.None); - public static bool operator !=(None left, None right) => !Equals(left, right); + public override string ToString() => Match(v => v?.ToString() ?? "", () => $"None {typeof(T).BeautifulName()}"); } public static class OptionExtension diff --git a/Source/Tests/FunicularSwitch.Test/ImplicitCastStudy.cs b/Source/Tests/FunicularSwitch.Test/ImplicitCastStudy.cs index 014ce28..19fc306 100644 --- a/Source/Tests/FunicularSwitch.Test/ImplicitCastStudy.cs +++ b/Source/Tests/FunicularSwitch.Test/ImplicitCastStudy.cs @@ -5,16 +5,17 @@ namespace FunicularSwitch.Test; [TestClass] public class ImplicitCastStudy { - [TestMethod] - [Ignore] //this test fails. The implicit case is never called here due to a legacy compiler behaviour regarding Nullable - public void ImplicitCastWithNullableStruct() - { - long? l = null; -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - Option converted = l; -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - converted.Should().NotBeNull(); - } +// Does not compile anymore because no auto conversion is possible +// [TestMethod] +// [Ignore] //this test fails. The implicit case is never called here due to a legacy compiler behaviour regarding Nullable +// public void ImplicitCastWithNullableStruct() +// { +// long? l = null; +// #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +// Option converted = l; +// #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. +// converted.Should().NotBeNull(); +// } [TestMethod] public void ImplicitCastWithClass() diff --git a/Source/Tests/FunicularSwitch.Test/OptionSpecs.cs b/Source/Tests/FunicularSwitch.Test/OptionSpecs.cs index bcc20f3..0a753d5 100644 --- a/Source/Tests/FunicularSwitch.Test/OptionSpecs.cs +++ b/Source/Tests/FunicularSwitch.Test/OptionSpecs.cs @@ -14,7 +14,7 @@ public void NullCoalescingWithOptionBoolBehavesAsExpected() bool? foo = null; var implicitTypedOption = foo ?? Option.None; - implicitTypedOption.GetType().Should().Be(typeof(None)); + implicitTypedOption.GetType().Should().Be(typeof(Option)); Option option = foo ?? Option.None; option.Equals(Option.None).Should().BeTrue();