Skip to content

Commit

Permalink
ToEquals for maybe and either monads
Browse files Browse the repository at this point in the history
- enable comparison of maybe monads with Nothing and raw values
- override ToString for maybe and either monads for meaningful assertion
  messages and diagnostics
- enable comparison of either values
  • Loading branch information
Dirk-Peters committed Sep 17, 2022
1 parent 526cf59 commit ef9bcf8
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 58 deletions.
27 changes: 27 additions & 0 deletions MonadicBits/Either.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using MonadicBits.Either;

namespace MonadicBits
Expand Down Expand Up @@ -86,6 +87,25 @@ public static implicit operator Either<TLeft, TRight>(Left<TLeft> left) =>

public static implicit operator Either<TLeft, TRight>(Right<TRight> right) =>
new Either<TLeft, TRight>(right.Value);

public override string ToString() =>
IsRight ? $"Right: {{{RightInstance}}}" : $"Left: {{{LeftInstance}}}";

public bool Equals(Either<TLeft, TRight> other) =>
IsRight == other.IsRight &&
(IsRight ? Equals(other.RightInstance, RightInstance) : Equals(other.LeftInstance, LeftInstance));

public override bool Equals(object obj) =>
obj switch
{
Either<TLeft, TRight> other => Equals(other),
Right<TRight> right when IsRight => Equals(right.Value, RightInstance),
Left<TLeft> left when !IsRight => Equals(left.Value, LeftInstance),
_ => false
};

public override int GetHashCode() =>
IsRight ? RightInstance.Right().GetHashCode() : LeftInstance.Left().GetHashCode();
}

namespace Either
Expand All @@ -97,6 +117,10 @@ public readonly struct Left<T>
public Left(T value) => Value = value;

public static implicit operator Left<T>(T value) => new Left<T>(value);

public Either<TResult, TRight>
BindLeft<TResult, TRight>([NotNull] Func<T, Either<TResult, TRight>> mapping) =>
Value.Left<T, TRight>().BindLeft(mapping);
}

public readonly struct Right<T>
Expand All @@ -106,6 +130,9 @@ public readonly struct Right<T>
public Right(T value) => Value = value;

public static implicit operator Right<T>(T value) => new Right<T>(value);

public Either<TLeft, TResult> Bind<TLeft, TResult>([NotNull] Func<T, Either<TLeft, TResult>> mapping) =>
Value.Right<TLeft, T>().Bind(mapping);
}
}
}
10 changes: 10 additions & 0 deletions MonadicBits/Enumerable.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
using System;
using System.Collections.Generic;

namespace MonadicBits
{
[Obsolete("use MonadicEnumerable instead")]
public static class Enumerable
{
public static IEnumerable<T> Return<T>(T value)
{
yield return value;
}
}

public static class MonadicEnumerable
{
public static IEnumerable<T> Return<T>(T value)
{
Expand Down
20 changes: 20 additions & 0 deletions MonadicBits/Maybe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,32 @@ public Either<TLeft, T> ToEither<TLeft>([NotNull] TLeft left) =>

public static implicit operator Maybe<T>(T value) =>
value == null ? Nothing : new Maybe<T>(value);

public override string ToString() => IsJust ? $"Just: {{{Instance}}}" : nameof(Nothing);

public override int GetHashCode() => IsJust ? Instance.GetHashCode() : 0;

public override bool Equals(object obj) =>
obj switch
{
Nothing _ => !IsJust,
Maybe<T> other => Equals(other),
T otherValue when IsJust => Equals(Instance, otherValue),
_ => false
};

public bool Equals(Maybe<T> other) =>
other.IsJust == IsJust && Equals(other.Instance, Instance);
}

namespace Maybe
{
public readonly struct Nothing
{
public override int GetHashCode() => 0;

public override bool Equals(object obj) =>
obj is Nothing || obj != null && obj.Equals(this);
}
}
}
106 changes: 86 additions & 20 deletions MonadicBitsTests/EitherTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using FluentAssertions;
using MonadicBits;
using NUnit.Framework;

namespace MonadicBitsTests
{
using static Functional;

public static class EitherTests
{
[Test]
Expand Down Expand Up @@ -60,8 +63,9 @@ public static void Map_right_either_with_null_mapping_throws_exception() =>
public static void Map_right_either_returns_either_with_new_type_and_value()
{
const int mappedValue = 42;
TestMonads.Right("Test").Map(_ => mappedValue)
.Match(Assert.Fail, i => Assert.AreEqual(mappedValue, i));
TestMonads.Right("Test")
.Map(_ => mappedValue)
.Should().Be(mappedValue.Right());
}

[Test]
Expand All @@ -80,15 +84,20 @@ public static void MapLeft_left_either_with_null_mapping_throws_exception() =>
public static void MapLeft_left_either_returns_either_with_new_type_and_value()
{
const int mappedValue = 42;
TestMonads.Left("Test").MapLeft(_ => mappedValue)
.Match(i => Assert.AreEqual(mappedValue, i), Assert.Fail);
TestMonads.Left("Test")
.MapLeft(_ => mappedValue)
.Should()
.Be(mappedValue.Left());
}

[Test]
public static void MapLeft_right_either_returns_same_right_either()
{
const string value = "Test";
TestMonads.Right(value).MapLeft(_ => 42).Match(_ => Assert.Fail(), s => Assert.AreEqual(value, s));
TestMonads.Right(value)
.MapLeft(_ => 42)
.Should()
.Be(value.Right());
}

[Test]
Expand All @@ -100,16 +109,18 @@ public static void Bind_right_either_with_null_mapping_throws_exception() =>
public static void Bind_right_either_to_method_returns_either_with_new_type_and_value()
{
const int bindValue = 42;
TestMonads.Right("Test").Bind(_ => bindValue.Right<string, int>())
.Match(Assert.Fail, i => Assert.AreEqual(bindValue, i));
TestMonads.Right("Test")
.Bind(_ => bindValue.Right<string, int>())
.Should().Be(bindValue.Right());
}

[Test]
public static void Bind_left_either_to_method_returns_same_left_either()
public static void Bind_on_left_changes_right_type_only()
{
const string value = "Test";
TestMonads.Left(value).Bind(_ => 42.Right<string, int>())
.Match(s => Assert.AreEqual(value, s), _ => Assert.Fail());
TestMonads.Left(value)
.Bind(_ => 42.Right<string, int>())
.Should().Be(value.Left<string, int>());
}

[Test]
Expand All @@ -118,30 +129,85 @@ public static void BindLeft_left_either_with_null_mapping_throws_exception() =>
TestMonads.Left("Test").BindLeft((Func<string, Either<string, string>>)null));

[Test]
public static void BindLeft_left_either_to_method_returns_either_with_new_type_and_value()
public static void BindLeft_on_left_changes_left_type_and_value()
{
const int bindValue = 42;
TestMonads.Left("Test").BindLeft(_ => bindValue.Left<int, string>())
.Match(i => Assert.AreEqual(bindValue, i), Assert.Fail);
TestMonads.Left("Test")
.BindLeft(_ => bindValue.Left<int, string>())
.Should().Be(bindValue.Left());
}

[Test]
public static void BindLeft_right_either_to_method_returns_same_right_either()
public static void BindLeft_on_right_changes_left_type_only()
{
const string value = "Test";
TestMonads.Right(value).BindLeft(_ => 42.Left<int, string>())
.Match(_ => Assert.Fail(), s => Assert.AreEqual(value, s));
TestMonads.Right(value)
.BindLeft(_ => 42.Left<int, string>())
.Should().Be(value.Right<int, string>());
}

[Test]
public static void Left_to_maybe_returns_empty_maybe() =>
TestMonads.Left("Test").ToMaybe().Match(Assert.Fail, Assert.Pass);
public static void Left_to_maybe_returns_nothing() =>
TestMonads.Left("Test").ToMaybe()
.Should().Be(Nothing);

[Test]
public static void Right_to_maybe_returns_maybe_with_value()
public static void Right_to_maybe_returns_just()
{
const string value = "Test";
TestMonads.Right(value).ToMaybe().Match(s => Assert.AreEqual(value, s), Assert.Fail);
TestMonads.Right(value)
.ToMaybe().Should().Be(value.Just());
}

[Test]
public static void BindLeft_on_explicit_left_changes_left_value_and_type() =>
42.Left().BindLeft(v => v.ToString().Left<string, string>())
.Should().Be(42.ToString().Left());

[Test]
public static void Bind_on_explicit_right_changes_value_and_type() =>
42.Right().Bind(v => v.ToString().Right<int, string>())
.Should().Be(42.ToString().Right());

[Test]
public static void Left_does_not_equal_arbitrary_right_of_same_type() =>
42.Left<int, string>().Should().NotBe("right".Right<int, string>());

[Test]
public static void Left_does_not_equal_arbitrary_value_of_correct_right_type() =>
42.Left<int, string>().Should().NotBe("right");

[Test]
public static void Right_does_not_equal_arbitrary_left_of_same_type() =>
42.Right<string, int>().Should().NotBe("right".Left<string, int>());

[Test]
public static void Right_does_not_equal_arbitrary_value_of_correct_left_type() =>
42.Right<string, int>().Should().NotBe("right");

[Test]
public static void Hash_codes_of_same_value_right_are_equal() =>
42.Right<int, int>().GetHashCode().Should().Be(42.Right<int, int>().GetHashCode());

[Test]
public static void Hash_codes_of_same_value_left_are_equal() =>
42.Left<int, int>().GetHashCode().Should().Be(42.Left<int, int>().GetHashCode());

[Test]
public static void Hash_codes_of_same_value_left_and_right_are_not_equal() =>
42.Left<int, int>().GetHashCode().Should().NotBe(42.Right<int, int>().GetHashCode());

[Test]
public static void String_representation_of_left_contains_value_representation() =>
42.Left<int, int>().ToString().Should().Contain(42.ToString());

[Test]
public static void String_representation_of_right_contains_value_representation() =>
42.Right<int, int>().ToString().Should().Contain(42.ToString());

[Test]
public static void String_representation_of_right_and_left_with_same_value_and_type_are_not_equal() =>
42.Right<int, int>().ToString()
.Should().NotBe(42.Left<int, int>().ToString());
}
}
2 changes: 1 addition & 1 deletion MonadicBitsTests/EnumerableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public static class EnumerableTests
{
[Test]
public static void Elevation_creates_single_item_enumeration() =>
MonadicBits.Enumerable.Return(42).Should().BeEquivalentTo(new[] { 42 });
MonadicBits.MonadicEnumerable.Return(42).Should().BeEquivalentTo(new[] { 42 });
}
}
19 changes: 10 additions & 9 deletions MonadicBitsTests/MaybeAsyncExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using FluentAssertions;
using MonadicBits;
using NUnit.Framework;
using static MonadicBitsTests.TestMonads;
Expand All @@ -13,62 +14,62 @@ public static async Task MapAsync_maybe_with_value_to_async_mapping_returns_mayb
{
const int input = 42;
(await "Test".Just().MapAsync(_ => Task.FromResult(input)))
.Match(i => Assert.AreEqual(input, i), Assert.Fail);
.Should().Be(input.Just());
}

[Test]
public static async Task MapAsync_empty_maybe_to_async_mapping_returns_empty_maybe_task() =>
(await Nothing<string>().MapAsync(_ => Task.FromResult(42)))
.Match(_ => Assert.Fail(), Assert.Pass);
.Should().Be(Functional.Nothing);

[Test]
public static async Task MapAsync_maybe_task_with_value_to_mapping_returns_maybe_task_with_new_value()
{
const int input = 42;
(await Task.FromResult("Test".Just()).MapAsync(_ => input))
.Match(i => Assert.AreEqual(input, i), Assert.Fail);
.Should().Be(input.Just());
}

[Test]
public static async Task MapAsync_maybe_task_with_value_to_async_mapping_returns_maybe_task_with_new_value()
{
const int input = 42;
(await Task.FromResult("Test".Just()).MapAsync(_ => Task.FromResult(input)))
.Match(i => Assert.AreEqual(input, i), Assert.Fail);
.Should().Be(input.Just());
}

[Test]
public static void BindAsync_with_null_mapping_throws_exception() =>
Assert.ThrowsAsync<ArgumentNullException>(() =>
"Test".Just().BindAsync((Func<string, Task<Maybe<string>>>) null));
"Test".Just().BindAsync((Func<string, Task<Maybe<string>>>)null));

[Test]
public static async Task BindAsync_maybe_with_value_to_async_mapping_returns_maybe_task_with_new_value()
{
const int input = 42;
(await "Test".Just().BindAsync(_ => Task.FromResult(input.Just())))
.Match(i => Assert.AreEqual(input, i), Assert.Fail);
.Should().Be(input.Just());
}

[Test]
public static async Task BindAsync_empty_maybe_to_async_mapping_empty_maybe_task() =>
(await Nothing<string>().BindAsync(_ => Task.FromResult(42.Just())))
.Match(_ => Assert.Fail(), Assert.Pass);
.Should().Be(Functional.Nothing);

[Test]
public static async Task BindAsync_maybe_task_with_value_to_mapping_returns_maybe_task_with_new_value()
{
const int input = 42;
(await Task.FromResult("Test".Just()).BindAsync(_ => input.Just()))
.Match(i => Assert.AreEqual(input, i), Assert.Fail);
.Should().Be(input.Just());
}

[Test]
public static async Task BindAsync_maybe_task_with_value_to_async_mapping_returns_maybe_task_with_new_value()
{
const int input = 42;
(await Task.FromResult("Test".Just()).BindAsync(_ => Task.FromResult(input.Just())))
.Match(i => Assert.AreEqual(input, i), Assert.Fail);
.Should().Be(input.Just());
}
}
}
Loading

0 comments on commit ef9bcf8

Please sign in to comment.