From 530ebcf16719c0ec1530cfe2f04b3fcdff16de1b Mon Sep 17 00:00:00 2001 From: David Retzlaff Date: Mon, 4 Mar 2024 15:52:20 +0100 Subject: [PATCH 1/3] Change option to readonly struct --- Source/FunicularSwitch/Option.cs | 104 ++++++------------ .../FunicularSwitch.Test/ImplicitCastStudy.cs | 21 ++-- .../Tests/FunicularSwitch.Test/OptionSpecs.cs | 2 +- 3 files changed, 47 insertions(+), 80 deletions(-) diff --git a/Source/FunicularSwitch/Option.cs b/Source/FunicularSwitch/Option.cs index 729d7c3..14692c8 100644 --- a/Source/FunicularSwitch/Option.cs +++ b/Source/FunicularSwitch/Option.cs @@ -4,34 +4,45 @@ using System.Linq; using System.Threading.Tasks; using FunicularSwitch.Extensions; +// ReSharper disable MemberCanBePrivate.Global 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(); + private readonly bool isSome; - public Option Map(Func map) => Match(t => Some(map(t)), None); + private readonly T value; - public Task> Map(Func> map) => Match(async t => Some(await map(t).ConfigureAwait(false)), () => Task.FromResult(None())); + private Option(T value) + { + isSome = true; + this.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 +51,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) { @@ -53,22 +63,19 @@ 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(); + return isSome ? some(value) : none(); } public TResult Match(Func some, TResult none) { - var iAmSome = this as Some; - return iAmSome != null ? some(iAmSome.Value) : none; + return 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 +83,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 +93,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; @@ -110,48 +115,9 @@ public async Task Match(Func> some, TResult n 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(); From 9afcfdad070972bce778be27ab13b7ac4bc5c373 Mon Sep 17 00:00:00 2001 From: David Retzlaff Date: Mon, 4 Mar 2024 15:54:15 +0100 Subject: [PATCH 2/3] Update major version to 6 --- Source/FunicularSwitch/FunicularSwitch.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 83aff18c4203ac5f2b8081e3cfc09215a26bd2e1 Mon Sep 17 00:00:00 2001 From: Alexander Wiedemann Date: Tue, 5 Mar 2024 08:50:23 +0100 Subject: [PATCH 3/3] - adapt naming --- .../MyError.cs | 2 +- Source/FunicularSwitch/Option.cs | 43 ++++++++----------- 2 files changed, 19 insertions(+), 26 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/Option.cs b/Source/FunicularSwitch/Option.cs index 14692c8..829f849 100644 --- a/Source/FunicularSwitch/Option.cs +++ b/Source/FunicularSwitch/Option.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading.Tasks; using FunicularSwitch.Extensions; -// ReSharper disable MemberCanBePrivate.Global namespace FunicularSwitch { @@ -22,19 +21,19 @@ public static class Option public static Option Some(T value) => new(value); - private readonly bool isSome; + readonly bool _isSome; - private readonly T value; + readonly T _value; - private Option(T value) + Option(T value) { - isSome = true; - this.value = value; + _isSome = true; + _value = value; } - public bool IsSome() => isSome; + public bool IsSome() => _isSome; - public bool IsNone() => !isSome; + public bool IsNone() => !_isSome; public Option Map(Func map) => Match(t => Option.Some(map(t)), Option.None); @@ -51,9 +50,9 @@ public void Match(Action some, Action? none = null) public async Task Match(Func some, Func? none = null) { - if (isSome) + if (_isSome) { - await some(value).ConfigureAwait(false); + await some(_value).ConfigureAwait(false); } else if (none != null) { @@ -61,21 +60,15 @@ public async Task Match(Func some, Func? none = null) } } - public TResult Match(Func some, Func none) - { - return isSome ? some(value) : none(); - } + public TResult Match(Func some, Func none) => _isSome ? some(_value) : none(); - public TResult Match(Func some, TResult none) - { - return isSome ? some(value) : none; - } + public TResult Match(Func some, TResult none) => _isSome ? some(_value) : none; public async Task Match(Func> some, Func> none) { - if (isSome) + if (_isSome) { - return await some(value).ConfigureAwait(false); + return await some(_value).ConfigureAwait(false); } return await none().ConfigureAwait(false); @@ -83,9 +76,9 @@ public async Task Match(Func> some, Func Match(Func> some, Func none) { - if (isSome) + if (_isSome) { - return await some(value).ConfigureAwait(false); + return await some(_value).ConfigureAwait(false); } return none(); @@ -93,9 +86,9 @@ public async Task Match(Func> some, Func Match(Func> some, TResult none) { - if (isSome) + if (_isSome) { - return await some(value).ConfigureAwait(false); + return await some(_value).ConfigureAwait(false); } return none; @@ -109,7 +102,7 @@ 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);