Skip to content

Commit

Permalink
Laws and laws documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
louthy committed Oct 28, 2024
1 parent f0d8b13 commit 4978442
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 88 deletions.
26 changes: 25 additions & 1 deletion LanguageExt.Core/Traits/Alternative/Alternative.Laws.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static Validation<Error, Unit> validate(Func<K<F, int>, K<F, int>, bool>?
{
equals ??= (fa, fb) => fa.Equals(fb);
return ApplicativeLaw<F>.validate(equals) >>
ChoiceLaw<F>.validate(equals) >>
leftCatchLaw(equals) >>
leftZeroLaw(equals) >>
rightZeroLaw(equals);
}
Expand Down Expand Up @@ -96,4 +96,28 @@ public static Validation<Error, Unit> rightZeroLaw(Func<K<F, int>, K<F, int>, bo
? unit
: Error.New($"Alternative right-zero law does not hold for {typeof(F).Name}");
}

/// <summary>
/// Left catch law
/// </summary>
/// <remarks>
/// choose(pure(x), pure(y)) = pure(x)
/// </remarks>
/// <remarks>
/// NOTE: `Equals` must be implemented for the `K<F, *>` derived-type, so that the laws
/// can be proven to be true. If your Alternative structure doesn't have `Equals` then
/// you must provide the optional `equals` parameter so that the equality of outcomes can
/// be tested.
/// </remarks>
public static Validation<Error, Unit> leftCatchLaw(Func<K<F, int>, K<F, int>, bool> equals)
{
var fa = F.Pure(100);
var fb = F.Pure(200);
var fr = choose(fa, fb);

return equals(fr, fa)
? unit
: Error.New($"Choice left-catch law does not hold for {typeof(F).Name}");
}

}
24 changes: 24 additions & 0 deletions LanguageExt.Core/Traits/Alternative/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
`Alternative<F>` allows for propagation of 'failure' and 'choice' (in some appropriate sense, depending on the type),
as well as provision of a unit/identity value (`Empty`).

`Alternative` is a `Choice` and `MonoidK`, which means it has a `Choose` method, a `Combine` method (which defaults to
calling the `Choose` method), and an `Empty` method. That creates a semantic meaning for `Choose`, which is about
choice propagation rather than the broader meaning of `SemigroupK.Combine`. It also allows for `Choose` and `Combine`
to have separate implementations depending on the type.

The way to think about `Choose` and the inherited `SemigroupK.Combine` methods is:
* `Choose` is the failure/choice propagation operator: `|`
* `Combine` is the concatenation/combination/addition operator: `+`

Any type that supports the `Alternative` trait should also implement the `|` operator, to enable easy choice/failure
propagation. If there is a different implementation of `Combine` (rather than accepting the default), then the type
should also implement the `+` operator.

`AlternativeLaw` can help you test your implementation:

choose(Pure(a), Pure(b)) = Pure(a)
choose(Empty(), Pure(b)) = Pure(b)
choose(Pure(a), Empty()) = Pure(a)
choose(Empty(), Empty()) = Empty()

It also tests the `Applicative` and `Functor` laws.
54 changes: 51 additions & 3 deletions LanguageExt.Core/Traits/Choice/Choice.Laws.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public static class ChoiceLaw<F>
/// you must provide the optional `equals` parameter so that the equality of outcomes can
/// be tested.
/// </remarks>
public static Unit assert(Func<K<F, int>, K<F, int>, bool>? equals = null) =>
validate(equals)
public static Unit assert(K<F, int> failure, Func<K<F, int>, K<F, int>, bool>? equals = null) =>
validate(failure, equals)
.IfFail(errors => errors.Throw());

/// <summary>
Expand All @@ -40,13 +40,61 @@ public static Unit assert(Func<K<F, int>, K<F, int>, bool>? equals = null) =>
/// you must provide the optional `equals` parameter so that the equality of outcomes can
/// be tested.
/// </remarks>
public static Validation<Error, Unit> validate(Func<K<F, int>, K<F, int>, bool>? equals = null)
public static Validation<Error, Unit> validate(K<F, int> failure, Func<K<F, int>, K<F, int>, bool>? equals = null)
{
equals ??= (fa, fb) => fa.Equals(fb);
return ApplicativeLaw<F>.validate(equals) >>
leftZeroLaw(failure, equals) >>
rightZeroLaw(failure, equals) >>
leftCatchLaw(equals);
}

/// <summary>
/// Left-zero law
/// </summary>
/// <remarks>
/// choose(empty, pure(x)) = pure(x)
/// </remarks>
/// <remarks>
/// NOTE: `Equals` must be implemented for the `K<F, *>` derived-type, so that the laws
/// can be proven to be true. If your Alternative structure doesn't have `Equals` then
/// you must provide the optional `equals` parameter so that the equality of outcomes can
/// be tested.
/// </remarks>
public static Validation<Error, Unit> leftZeroLaw(K<F, int> failure, Func<K<F, int>, K<F, int>, bool> equals)
{
var fa = failure;
var fb = F.Pure(100);
var fr = choose(fa, fb);

return equals(fr, fb)
? unit
: Error.New($"Choice left-zero law does not hold for {typeof(F).Name}");
}

/// <summary>
/// Right-zero law
/// </summary>
/// <remarks>
/// choose(pure(x), empty) = pure(x)
/// </remarks>
/// <remarks>
/// NOTE: `Equals` must be implemented for the `K<F, *>` derived-type, so that the laws
/// can be proven to be true. If your Alternative structure doesn't have `Equals` then
/// you must provide the optional `equals` parameter so that the equality of outcomes can
/// be tested.
/// </remarks>
public static Validation<Error, Unit> rightZeroLaw(K<F, int> failure, Func<K<F, int>, K<F, int>, bool> equals)
{
var fa = F.Pure(100);
var fb = failure;
var fr = choose(fa, fb);

return equals(fr, fa)
? unit
: Error.New($"Choice right-zero law does not hold for {typeof(F).Name}");
}

/// <summary>
/// Left catch law
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions LanguageExt.Core/Traits/Choice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
`Choice<F>` allows for propagation of 'failure' and 'choice' (in some appropriate sense, depending on the type).

`Choice` is a `SemigroupK`, but has a `Choose` method, rather than relying on the `SemigroupK.Combine` method, (which
now has a default implementation of invoking `Choose`). That creates a new semantic meaning for `Choose`, which is
about choice propagation rather than the broader meaning of `Combine`. It also allows for `Choose` and `Combine` to
have separate implementations depending on the type.

The way to think about `Choose` and the inherited `SemigroupK.Combine` methods is:
* `Choose` is the failure/choice propagation operator: `|`
* `Combine` is the concatenation/combination/addition operator: `+`

Any type that supports the `Choice` trait should also implement the `|` operator, to enable easy choice/failure
propagation. If there is a different implementation of `Combine` (rather than accepting the default), then the type
should also implement the `+` operator.

`ChoiceLaw` can help you test your implementation:

choose(Pure(a), Pure(b)) = Pure(a)
choose(Fail, Pure(b)) = Pure(b)
choose(Pure(a), Fail) = Pure(a)
choose(Fail [1], Fail [2]) = Fail [2]

It also tests the `Applicative` and `Functor` laws.
101 changes: 20 additions & 81 deletions LanguageExt.Tests/TraitTests/ChoiceLawTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using LanguageExt.Common;
using Xunit;

namespace LanguageExt.Tests.TraitTests;
Expand All @@ -6,31 +7,31 @@ public class ChoiceLawTests
{
[Fact]
public void Arr() =>
ChoiceLaw<Arr>.assert();
ChoiceLaw<Arr>.assert(Arr<int>.Empty);

[Fact]
public void HashSet() =>
ChoiceLaw<HashSet>.assert();
ChoiceLaw<HashSet>.assert(HashSet<int>.Empty);

[Fact]
public void Iterable() =>
ChoiceLaw<Iterable>.assert();
ChoiceLaw<Iterable>.assert(Iterable<int>.Empty);

[Fact]
public void Lst() =>
ChoiceLaw<Lst>.assert();
ChoiceLaw<Lst>.assert(Lst<int>.Empty);

[Fact]
public void Seq() =>
ChoiceLaw<Seq>.assert();
ChoiceLaw<Seq>.assert(Seq<int>.Empty);

[Fact]
public void EffRT()
{
bool eq(K<Eff<Unit>, int> vx, K<Eff<Unit>, int> vy) =>
vx.Run(unit).Equals(vy.Run(unit));

ChoiceLaw<Eff<Unit>>.assert(eq);
ChoiceLaw<Eff<Unit>>.assert(error<Eff<Unit>, int>(Errors.None), eq);
}

[Fact]
Expand All @@ -39,7 +40,7 @@ public void Eff()
bool eq(K<Eff, int> vx, K<Eff, int> vy) =>
vx.Run().Equals(vy.Run());

ChoiceLaw<Eff>.assert(eq);
ChoiceLaw<Eff>.assert(error<Eff, int>(Errors.None), eq);
}

[Fact]
Expand All @@ -48,118 +49,56 @@ public void IO()
bool eq(K<IO, int> vx, K<IO, int> vy) =>
vx.RunSafe().Equals(vy.RunSafe());

ChoiceLaw<IO>.assert(eq);
ChoiceLaw<IO>.assert(error<IO, int>(Errors.None), eq);
}

[Fact]
public void Either() =>
ChoiceLaw<Either<string>>.assert();
ChoiceLaw<Either<string>>.assert(fail<string, Either<string>, int>("error"));

[Fact]
public void EitherT() =>
ChoiceLaw<EitherT<string, Identity>>.assert();
ChoiceLaw<EitherT<string, Identity>>.assert(fail<string, EitherT<string, Identity>, int>("error"));

[Fact]
public void Fin() =>
ChoiceLaw<Fin>.assert();
ChoiceLaw<Fin>.assert(error<Fin, int>(Errors.None));

[Fact]
public void FinT() =>
ChoiceLaw<FinT<Identity>>.assert();
ChoiceLaw<FinT<Identity>>.assert(error<FinT<Identity>, int>(Errors.None));

[Fact]
public void Option() =>
ChoiceLaw<Option>.assert();
ChoiceLaw<Option>.assert(fail<Unit, Option, int>(unit));

[Fact]
public void OptionT() =>
ChoiceLaw<OptionT<Identity>>.assert();
ChoiceLaw<OptionT<Identity>>.assert(fail<Unit, OptionT<Identity>, int>(unit));

[Fact]
public void Try()
{
bool eq(K<Try, int> vx, K<Try, int> vy) =>
vx.Run().Equals(vy.Run());

ChoiceLaw<Try>.assert(eq);
ChoiceLaw<Try>.assert(error<Try, int>(Errors.None), eq);
}

[Fact]
public void TryT()
{
bool eq(K<TryT<Identity>, int> vx, K<TryT<Identity>, int> vy) =>
vx.Run().Run().Equals(vy.Run().Run());
ChoiceLaw<TryT<Identity>>.assert(eq);

ChoiceLaw<TryT<Identity>>.assert(error<TryT<Identity>, int>(Errors.None), eq);
}

[Fact]
public void Validation() =>
ChoiceLaw<Validation<StringM>>.assert();
ChoiceLaw<Validation<StringM>>.assert(fail<StringM, Validation<StringM>, int>("error"));

[Fact]
public void ValidationT() =>
ChoiceLaw<ValidationT<StringM, Identity>>.assert();

[Fact]
public void Identity() =>
ChoiceLaw<Identity>.assert();

[Fact]
public void IdentityT() =>
ChoiceLaw<IdentityT<Identity>>.assert();

[Fact]
public void Reader()
{
bool eq(K<Reader<string>, int> vx, K<Reader<string>, int> vy) =>
vx.Run("Hello").Equals(vy.Run("Hello"));

ChoiceLaw<Reader<string>>.assert(eq);
}

[Fact]
public void ReaderT()
{
bool eq(K<ReaderT<string, Identity>, int> vx, K<ReaderT<string, Identity>, int> vy) =>
vx.Run("Hello").Run().Equals(vy.Run("Hello").Run());

ChoiceLaw<ReaderT<string, Identity>>.assert(eq);
}

[Fact]
public void State()
{
bool eq(K<State<string>, int> vx, K<State<string>, int> vy) =>
vx.Run("Hello").Equals(vy.Run("Hello"));

ChoiceLaw<State<string>>.assert(eq);
}

[Fact]
public void StateT()
{
bool eq(K<StateT<string, Identity>, int> vx, K<StateT<string, Identity>, int> vy) =>
vx.Run("Hello").Run().Equals(vy.Run("Hello").Run());

ChoiceLaw<StateT<string, Identity>>.assert(eq);
}

[Fact]
public void Writer()
{
bool eq(K<Writer<StringM>, int> vx, K<Writer<StringM>, int> vy) =>
vx.Run("Hello, ").Equals(vy.Run("Hello, "));

ChoiceLaw<Writer<StringM>>.assert(eq);
}

[Fact]
public void WriterT()
{
bool eq(K<WriterT<StringM, Identity>, int> vx, K<WriterT<StringM, Identity>, int> vy) =>
vx.Run("Hello, ").Run().Equals(vy.Run("Hello, ").Run());

ChoiceLaw<WriterT<StringM, Identity>>.assert(eq);
}
ChoiceLaw<ValidationT<StringM, Identity>>.assert(fail<StringM, ValidationT<StringM, Identity>, int>("error"));
}
12 changes: 9 additions & 3 deletions Samples/TestBed/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using LanguageExt.Sys.Live;
using System.Threading.Tasks;
using LanguageExt.Common;
using LanguageExt.Traits;
using TestBed;
using static LanguageExt.Prelude;
using static LanguageExt.Pipes.Proxy;
Expand Down Expand Up @@ -75,9 +76,14 @@ public class Program
{
static void Main(string[] args)
{
IEnumerable<int> ma = [];
IEnumerable<int> mb = [];
var mc = ma.SelectMany(a => mb.Select(b => a + b));
//var mx = ReaderT<Unit, OptionT<Seq>, int>.Lift(OptionT<Seq, int>.None);
//var my = ReaderT<Unit, OptionT<Seq>, int>.Lift(OptionT<Seq, int>.Lift(Seq(6, 7, 8, 9, 10)));
var mx = ReaderT<Unit, Seq, int>.Lift(Seq(1, 2, 3, 4, 5));
var my = ReaderT<Unit, Seq, int>.Lift(Seq(6, 7, 8, 9, 10));
var mr = mx + my;

Console.WriteLine(mr.Run(unit));


////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
Expand Down

0 comments on commit 4978442

Please sign in to comment.