[LINQ via C# series]
[Category Theory via C# series]
Latest version: https://weblogs.asp.net/dixin/category-theory-via-csharp-2-monoid
Monoid and monoid laws
A monoid, denoted a 3-tuple (M, ⊙, I), is a set M with
- a binary operator ⊙ : M ⊙ M → M
- This operation M ⊙ M → M is denoted μ
- and a special element unit, denoted I, I ∈ M
- I → M is denoted η
satisfying:
- left unit law λX: I ⊙ X ≌ X
- right unit law ρX: X ≌ X ⊙ I
- associative law αX, Y, Z: (X ⊙ Y) ⊙ Z ≌ X ⊙ (Y ⊙ Z)
so that:
This is quite general and abstract. A intuitive example is the set of all integers, with operator + and unit 0. So this 3-tuple (integer, +, 0) satisfies:
- 0 + x ≌ x
- x ≌ x + 0
- (x + y) + z ≌ x + (y + z)
where x, y, z are elements of the set of integers. Therefore (integer, +, 0) is a monoid.
A monoid can be represented in C# as:
public partial interface IMonoid<T>{ T Unit { [Pure] get; }
Func<T, T, T> Binary { [Pure] get; }}A default implementation is straight forward:
public partial class Monoid<T> : IMonoid<T>{ public Monoid(T unit, [Pure] Func<T, T, T> binary) { this.Unit = unit; this.Binary = binary; }
public T Unit { [Pure] get; }
public Func<T, T, T> Binary { [Pure] get; }}C#/.NET monoids
First of all, an extension method is created for convenience:
[Pure]public static class MonoidExtensions{ public static IMonoid<T> Monoid<T>(this T unit, Func<T, T, T> binary) { return new Monoid<T>(unit, binary); }}Void and Unit monoids
Theoretically System.Void can be a monoid. Its source code is:
public struct Void{}which leads to only one way to get the Void value:
Void value = new Void();So a monoid can be constructed as:
IMonoid<Void> voidMonoid = new Void().Monoid((a, b) => new Void());However, C# compiler does not allow System.Void to be used like this. There are 2 workarounds:
- Copy above Void definition to local
- Use Microsoft.FSharp.Core.Unit to replace System.Void
F#’s unit is equivalent to C#’s void, and Microsoft.FSharp.Core.Unit is semantically close to System.Void. Unit’s source code is:
type Unit() = override x.GetHashCode() = 0 override x.Equals(obj:obj) = match obj with null -> true | :? Unit -> true | _ -> false interface System.IComparable with member x.CompareTo(_obj:obj) = 0
and unit = UnitThe difference is, Unit is a class, and its only possible value is null.
Unit unit = null;So a monoid can be constructed by Unit too:
IMonoid<Unit> unitMonoid = ((Unit)null).Monoid((a, b) => null);More examples
As fore mentioned, (int, +, 0) is a monoid:
IMonoid<int> addInt32 = 0.Monoid((a, b) => a + b);Assert.AreEqual(0, addInt32.Unit);Assert.AreEqual(1 + 2, addInt32.Binary(1, 2));
// Monoid law 1: Unit Binary m == mAssert.AreEqual(1, addInt32.Binary(addInt32.Unit, 1));// Monoid law 2: m Binary Unit == mAssert.AreEqual(1, addInt32.Binary(1, addInt32.Unit));// Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3)Assert.AreEqual(addInt32.Binary(addInt32.Binary(1, 2), 3), addInt32.Binary(1, addInt32.Binary(2, 3)));Brian Beckman had a clock monoid in a video - consider numbers on the clock:
If a ⊙ b is defined as a => b => (a + b) % 12, then 12 becomes the unit. So:
IMonoid<int> clock = 12.Monoid((a, b) => (a + b) % 12);Here are more similar examples:
- (int, *, 1)
- (string, string.Concat, string.Empty)
- (bool, ||, false)
- (bool, &&, true)
- (IEnumerable
, Enumerable.Concat, Enumerable.Empty ())
Nullable monoid
And monoid (Nullable
First of all, the built-in System.Nullable<> only works for value type, since reference type can naturally be null. Here for the category theory discussion, a Nullable
public class Nullable<T>{ private readonly Lazy<Tuple<bool, T>> factory;
public Nullable(Func<Tuple<bool, T>> factory = null) { this.factory = factory == null ? null : new Lazy<Tuple<bool, T>>(factory); }
public bool HasValue { [Pure] get { return this.factory?.Value != null && this.factory.Value.Item1 && this.factory.Value.Item2 != null; } }
public T Value { [Pure] get { // Message is copied from mscorlib.dll string table, where key is InvalidOperation_NoValue. Contract.Requires<InvalidOperationException>(this.HasValue, "Nullable object must have a value.");
return this.factory.Value.Item2; } }}This Nullable
- When factory function is not provided (null), Nullable
does not have value. - When factory function is provided, the function returns a tuple if executed.
- The tuple’s bool value indicates there is a value available (because when T is a value type, the other item in the tuple cannot be null).
- When the bool is true and the other T value is not null, Nullable
has a value.
Below is one way to define the binary operator ⊙, taking new Nullable
[Pure]public static partial class MonoidExtensions{ public static IMonoid<T> Monoid<T> (this T unit, Func<T, T, T> binary) => new Monoid<T>(unit, binary);
public static IMonoid<Nullable<TSource>> MonoidOfNullable<TSource> (this IMonoid<TSource> monoid) => new Monoid<Nullable<TSource>>( new Nullable<TSource>(), (a, b) => new Nullable<TSource>(() => { if (a.HasValue && b.HasValue) { return Tuple.Create(true, monoid.Binary(a.Value, b.Value)); }
if (a.HasValue) { return Tuple.Create(true, a.Value); }
if (b.HasValue) { return Tuple.Create(true, b.Value); }
return Tuple.Create(false, default(TSource)); }));}So that (Nullable
Unit tests
These unit tests demonstrate how the monoids are constructed and how the monoid laws are satisfied:
[TestClass]public class MonoidTests{ [TestMethod()] public void StringTest() { IMonoid<string> concatString = string.Empty.Monoid((a, b) => string.Concat(a, b)); Assert.AreEqual(string.Empty, concatString.Unit); Assert.AreEqual("ab", concatString.Binary("a", "b"));
// Monoid law 1: Unit Binary m == m Assert.AreEqual("ab", concatString.Binary(concatString.Unit, "ab")); // Monoid law 2: m Binary Unit == m Assert.AreEqual("ab", concatString.Binary("ab", concatString.Unit)); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) Assert.AreEqual(concatString.Binary(concatString.Binary("a", "b"), "c"), concatString.Binary("a", concatString.Binary("b", "c"))); }
[TestMethod()] public void Int32Test() { IMonoid<int> addInt32 = 0.Monoid((a, b) => a + b); Assert.AreEqual(0, addInt32.Unit); Assert.AreEqual(1 + 2, addInt32.Binary(1, 2));
// Monoid law 1: Unit Binary m == m Assert.AreEqual(1, addInt32.Binary(addInt32.Unit, 1)); // Monoid law 2: m Binary Unit == m Assert.AreEqual(1, addInt32.Binary(1, addInt32.Unit)); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) Assert.AreEqual(addInt32.Binary(addInt32.Binary(1, 2), 3), addInt32.Binary(1, addInt32.Binary(2, 3)));
IMonoid<int> multiplyInt32 = 1.Monoid((a, b) => a * b); Assert.AreEqual(1, multiplyInt32.Unit); Assert.AreEqual(1 * 2, multiplyInt32.Binary(1, 2));
// Monoid law 1: Unit Binary m == m Assert.AreEqual(2, multiplyInt32.Binary(multiplyInt32.Unit, 2)); // Monoid law 2: m Binary Unit == m Assert.AreEqual(2, multiplyInt32.Binary(2, multiplyInt32.Unit)); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) Assert.AreEqual(multiplyInt32.Binary(multiplyInt32.Binary(1, 2), 3), multiplyInt32.Binary(1, multiplyInt32.Binary(2, 3))); }
[TestMethod()] public void ClockTest() { // Stolen from: http://channel9.msdn.com/Shows/Going+Deep/Brian-Beckman-Dont-fear-the-Monads IMonoid<int> clock = 12.Monoid((a, b) => (a + b) % 12); Assert.AreEqual(12, clock.Unit); Assert.AreEqual((7 + 10) % 12, clock.Binary(7, 10));
// Monoid law 1: Unit Binary m == m Assert.AreEqual(111 % 12, clock.Binary(clock.Unit, 111)); // Monoid law 2: m Binary Unit == m Assert.AreEqual(111 % 12, clock.Binary(111, clock.Unit)); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) Assert.AreEqual(clock.Binary(clock.Binary(11, 22), 33), clock.Binary(11, clock.Binary(22, 33))); }
[TestMethod()] public void BooleanTest() { IMonoid<bool> orBoolean = false.Monoid((a, b) => a || b); Assert.IsFalse(orBoolean.Unit); Assert.AreEqual(true || false, orBoolean.Binary(true, false));
// Monoid law 1: Unit Binary m == m Assert.AreEqual(true, orBoolean.Binary(orBoolean.Unit, true)); Assert.AreEqual(false, orBoolean.Binary(orBoolean.Unit, false)); // Monoid law 2: m Binary Unit == m Assert.AreEqual(true, orBoolean.Binary(true, orBoolean.Unit)); Assert.AreEqual(false, orBoolean.Binary(false, orBoolean.Unit)); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) Assert.AreEqual(orBoolean.Binary(orBoolean.Binary(true, false), true), orBoolean.Binary(true, orBoolean.Binary(false, true)));
IMonoid<bool> andBoolean = true.Monoid((a, b) => a && b); Assert.IsTrue(andBoolean.Unit); Assert.AreEqual(true && false, andBoolean.Binary(true, false));
// Monoid law 1: Unit Binary m == m Assert.AreEqual(true, andBoolean.Binary(andBoolean.Unit, true)); Assert.AreEqual(false, andBoolean.Binary(andBoolean.Unit, false)); // Monoid law 2: m Binary Unit == m Assert.AreEqual(true, andBoolean.Binary(true, andBoolean.Unit)); Assert.AreEqual(false, andBoolean.Binary(false, andBoolean.Unit)); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) Assert.AreEqual(andBoolean.Binary(andBoolean.Binary(true, false), true), andBoolean.Binary(true, andBoolean.Binary(false, true))); }
[TestMethod()] public void EnumerableTest() { IMonoid<IEnumerable<int>> concatEnumerable = Enumerable.Empty<int>().Monoid((a, b) => a.Concat(b)); Assert.IsFalse(concatEnumerable.Unit.Any()); int[] x = new int[] { 0, 1, 2 }; int[] y = new int[] { 3, 4, 5 }; EnumerableAssert.AreEqual(concatEnumerable.Binary(x, y), x.Concat(y));
// Monoid law 1: Unit Binary m == m EnumerableAssert.AreEqual(concatEnumerable.Binary(concatEnumerable.Unit, x), x); // Monoid law 2: m Binary Unit == m EnumerableAssert.AreEqual(concatEnumerable.Binary(x, concatEnumerable.Unit), x); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) EnumerableAssert.AreEqual( concatEnumerable.Binary(concatEnumerable.Binary(x, y), x), concatEnumerable.Binary(x, concatEnumerable.Binary(y, x))); }
[TestMethod()] public void NullableTest() { IMonoid<int> addInt32 = 0.Monoid((a, b) => a + b); IMonoid<Nullable<int>> addNullable = addInt32.MonoidOfNullable(); Assert.IsFalse(addNullable.Unit.HasValue); Assert.AreEqual(addInt32.Binary(1, 2), addNullable.Binary(1.Nullable(), 2.Nullable()).Value); Assert.AreEqual(1, addNullable.Binary(1.Nullable(), new Nullable<int>()).Value); Assert.AreEqual(2, addNullable.Binary(new Nullable<int>(), 2.Nullable()).Value); Assert.IsFalse(addNullable.Binary(new Nullable<int>(), new Nullable<int>()).HasValue);
// Monoid law 1: Unit Binary m == m Assert.AreEqual(1, addNullable.Binary(addNullable.Unit, 1.Nullable()).Value); // Monoid law 2: m Binary Unit == m Assert.AreEqual(1, addNullable.Binary(1.Nullable(), addNullable.Unit).Value); // Monoid law 3: (m1 Binary m2) Binary m3 == m1 Binary (m2 Binary m3) Nullable<int> left = addNullable.Binary(addNullable.Binary(1.Nullable(), 2.Nullable()), 3.Nullable()); Nullable<int> right = addNullable.Binary(1.Nullable(), addNullable.Binary(2.Nullable(), 3.Nullable())); Assert.AreEqual(left.Value, right.Value); }}