Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/option as struct #37

Merged
merged 3 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading