Skip to content

Commit

Permalink
Feature/option as struct (#37)
Browse files Browse the repository at this point in the history
* Change option to readonly struct

* Update major version to 6

* - adapt naming

---------

Co-authored-by: Alexander Wiedemann <[email protected]>
  • Loading branch information
Tyrrx and ax0l0tl authored Mar 5, 2024
1 parent 3d2915a commit df7afb8
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 90 deletions.
2 changes: 1 addition & 1 deletion Source/FunicularSwitch.Generators.Templates/MyError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions Source/FunicularSwitch/FunicularSwitch.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
</VersionSuffixLocal>

<!--#region adapt versions here-->
<MajorVersion>5</MajorVersion>
<MinorAndPatchVersion>1.1</MinorAndPatchVersion>
<MajorVersion>6</MajorVersion>
<MinorAndPatchVersion>0.0</MinorAndPatchVersion>
<!--#endregion-->

<AssemblyVersion>$(MajorVersion).0.0</AssemblyVersion>
Expand Down
111 changes: 35 additions & 76 deletions Source/FunicularSwitch/Option.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,41 @@

namespace FunicularSwitch
{
public abstract class Option
public static class Option
{
public static Option<T> Some<T>(T value) => new Some<T>(value);
public static Option<T> Some<T>(T value) => Option<T>.Some(value);
public static Option<T> None<T>() => Option<T>.None;
public static async Task<Option<T>> Some<T>(Task<T> value) => Some(await value);
public static Task<Option<T>> NoneAsync<T>() => Task.FromResult(Option<T>.None);
}

public abstract class Option<T> : Option, IEnumerable<T>
public readonly struct Option<T> : IEnumerable<T>
{
#pragma warning disable CS0109 // Member does not hide an inherited member; new keyword is not required
public new static readonly Option<T> None = new None<T>();
#pragma warning restore CS0109 // Member does not hide an inherited member; new keyword is not required
public static readonly Option<T> None = default;

public bool IsSome() => GetType() == typeof(Some<T>);
public static Option<T> Some(T value) => new(value);

public bool IsNone() => !IsSome();
readonly bool _isSome;

public Option<T1> Map<T1>(Func<T, T1> map) => Match(t => Some(map(t)), None<T1>);
readonly T _value;

public Task<Option<T1>> Map<T1>(Func<T, Task<T1>> map) => Match(async t => Some(await map(t).ConfigureAwait(false)), () => Task.FromResult(None<T1>()));
Option(T value)
{
_isSome = true;
_value = value;
}

public bool IsSome() => _isSome;

public bool IsNone() => !_isSome;

public Option<T1> Bind<T1>(Func<T, Option<T1>> map) => Match(map, None<T1>);
public Option<T1> Map<T1>(Func<T, T1> map) => Match(t => Option<T1>.Some(map(t)), Option<T1>.None);

public Task<Option<T1>> Bind<T1>(Func<T, Task<Option<T1>>> bind) => Match(bind, () => None<T1>());
public Task<Option<T1>> Map<T1>(Func<T, Task<T1>> map) => Match(async t => Option<T1>.Some(await map(t).ConfigureAwait(false)), () => Task.FromResult(Option<T1>.None));

public Option<T1> Bind<T1>(Func<T, Option<T1>> map) => Match(map, Option<T1>.None);

public Task<Option<T1>> Bind<T1>(Func<T, Task<Option<T1>>> bind) => Match(bind, () => Option<T1>.None);

public void Match(Action<T> some, Action? none = null)
{
Expand All @@ -40,57 +50,45 @@ public void Match(Action<T> some, Action? none = null)

public async Task Match(Func<T, Task> some, Func<Task>? none = null)
{
var iAmSome = this as Some<T>;
if (iAmSome != null)
if (_isSome)
{
await some(iAmSome.Value).ConfigureAwait(false);
await some(_value).ConfigureAwait(false);
}
else if (none != null)
{
await none().ConfigureAwait(false);
}
}

public TResult Match<TResult>(Func<T, TResult> some, Func<TResult> none)
{
var iAmSome = this as Some<T>;
return iAmSome != null ? some(iAmSome.Value) : none();
}
public TResult Match<TResult>(Func<T, TResult> some, Func<TResult> none) => _isSome ? some(_value) : none();

public TResult Match<TResult>(Func<T, TResult> some, TResult none)
{
var iAmSome = this as Some<T>;
return iAmSome != null ? some(iAmSome.Value) : none;
}
public TResult Match<TResult>(Func<T, TResult> some, TResult none) => _isSome ? some(_value) : none;

public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, Func<Task<TResult>> none)
{
var iAmSome = this as Some<T>;
if (iAmSome != null)
if (_isSome)
{
return await some(iAmSome.Value).ConfigureAwait(false);
return await some(_value).ConfigureAwait(false);
}

return await none().ConfigureAwait(false);
}

public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, Func<TResult> none)
{
var iAmSome = this as Some<T>;
if (iAmSome != null)
if (_isSome)
{
return await some(iAmSome.Value).ConfigureAwait(false);
return await some(_value).ConfigureAwait(false);
}

return none();
}

public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, TResult none)
{
var iAmSome = this as Some<T>;
if (iAmSome != null)
if (_isSome)
{
return await some(iAmSome.Value).ConfigureAwait(false);
return await some(_value).ConfigureAwait(false);
}

return none;
Expand All @@ -104,54 +102,15 @@ public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, TResult n

public T? GetValueOrDefault() => Match(v => (T?)v, () => default);

public T GetValueOrDefault(Func<T> defaultValue) => Match(v => v, () => defaultValue());
public T GetValueOrDefault(Func<T> 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<TOther> Convert<TOther>() => Match(s => Some((TOther)(object)s!), None<TOther>);

public override string ToString() => Match(v => v?.ToString() ?? "", () => $"None {GetType().BeautifulName()}");
}

public sealed class Some<T> : Option<T>
{
public T Value { get; }

public Some(T value) => Value = value;

bool Equals(Some<T> other) => EqualityComparer<T>.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<T>)obj);
}

public override int GetHashCode() => EqualityComparer<T>.Default.GetHashCode(Value);

public static bool operator ==(Some<T>? left, Some<T>? right) => Equals(left, right);

public static bool operator !=(Some<T>? left, Some<T>? right) => !Equals(left, right);
}

public sealed class None<T> : Option<T>
{
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<T>).GetHashCode();

public static bool operator ==(None<T> left, None<T> right) => Equals(left, right);
public Option<TOther> Convert<TOther>() => Match(s => Option<TOther>.Some((TOther)(object)s!), Option<TOther>.None);

public static bool operator !=(None<T> left, None<T> right) => !Equals(left, right);
public override string ToString() => Match(v => v?.ToString() ?? "", () => $"None {typeof(T).BeautifulName()}");
}

public static class OptionExtension
Expand Down
21 changes: 11 additions & 10 deletions Source/Tests/FunicularSwitch.Test/ImplicitCastStudy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>
public void ImplicitCastWithNullableStruct()
{
long? l = null;
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
Option<long> 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<T>
// public void ImplicitCastWithNullableStruct()
// {
// long? l = null;
// #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
// Option<long> converted = l;
// #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
// converted.Should().NotBeNull();
// }

[TestMethod]
public void ImplicitCastWithClass()
Expand Down
2 changes: 1 addition & 1 deletion Source/Tests/FunicularSwitch.Test/OptionSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public void NullCoalescingWithOptionBoolBehavesAsExpected()
bool? foo = null;

var implicitTypedOption = foo ?? Option<bool>.None;
implicitTypedOption.GetType().Should().Be(typeof(None<bool>));
implicitTypedOption.GetType().Should().Be(typeof(Option<bool>));

Option<bool> option = foo ?? Option<bool>.None;
option.Equals(Option<bool>.None).Should().BeTrue();
Expand Down

0 comments on commit df7afb8

Please sign in to comment.