Category Theory via C# (13) Monoidal Functor-like Tuple<> And Task<>
[LINQ via C# series]
[Category Theory via C# series]
Latest version: https://weblogs.asp.net/dixin/category-theory-via-csharp-6-monoidal-functor-and-applicative-functor
Tuple<>: lack of laziness
Theoretically, Tuple<> should be counted as the Id<> monoidal functor. However, as previously mentioned, it is lack of laziness.
// [Pure] public static partial class TupleExtensions { public static Tuple<TResult> Apply<TSource, TResult> (this Tuple<Func<TSource, TResult>> selectorFunctor, Tuple<TSource> source) => new Tuple<TResult>(selectorFunctor.Item1(source.Item1)); public static Tuple<T> Tuple<T> (this T value) => new Tuple<T>(value); // φ: Lazy<Tuple<T1>, Tuple<T2>> => Tuple<Lazy<T1, T2>> public static Tuple<Lazy<T1, T2>> Binary<T1, T2> (this Lazy<Tuple<T1>, Tuple<T2>> binaryFunctor) => new Func<T1, Func<T2, Lazy<T1, T2>>>(x => y => new Lazy<T1, T2>(x, y)) .Tuple() .Apply(binaryFunctor.Value1) .Apply(binaryFunctor.Value2); // ι: Unit -> Tuple<Unit> public static Tuple<Unit> Unit (Unit unit) => unit.Tuple(); }
Tuple<> is most close to the Haskell Id Applicative.
Task<>: lack of purity
Task<> also seems monoidal functor, but is lack of purity:
// Impure. public static partial class TaskExtensions { public static async Task<TResult> Apply<TSource, TResult> (this Task<Func<TSource, TResult>> selectorFunctor, Task<TSource> source) => (await selectorFunctor)(await source); public static Task<T> Task<T> (this T value) => System.Threading.Tasks.Task.FromResult(value); // φ: Lazy<Task<T1>, Task<T2>> => Task<Lazy<T1, T2>> public static Task<Lazy<T1, T2>> Binary<T1, T2> (this Lazy<Task<T1>, Task<T2>> binaryFunctor) => new Func<T1, Func<T2, Lazy<T1, T2>>>(x => y => new Lazy<T1, T2>(x, y)) .Task() .Apply(binaryFunctor.Value1) .Apply(binaryFunctor.Value2); // ι: Unit -> Func<Unit> public static Task<Unit> Unit (Unit unit) => unit.Task(); }
Unit tests
Following unit tests demonstrate the usage of Tuple<> and Task<>. Notice Tuple is lack of laziness, and Task<>’s extension methods work for both cold tasks and hot tasks.
public partial class MonoidalFunctorTests { [TestMethod()] public void TupleTest() { bool isExecuted1 = false; Func<int, int> addOne = x => { isExecuted1 = true; return x + 1; }; Tuple<int> query1 = addOne.Tuple().Apply(2.Tuple()); Assert.IsTrue(isExecuted1); // No laziness. Assert.AreEqual(2 + 1, query1.Item1); // Execution. Assert.IsTrue(isExecuted1); // f.Functor().Apply(F) == F.Select(f) Assert.AreEqual(addOne.Tuple().Apply(1.Tuple()).Item1, 1.Tuple().Select(addOne).Item1); // id.Functor().Apply(F) == F Func<int, int> id = Functions.Id; Assert.AreEqual(id.Tuple().Apply(1.Tuple()).Item1, 1.Tuple().Item1); // o.Functor().Apply(F1).Apply(F2).Apply(F3) == F1.Apply(F2.Apply(F3)) Func<int, int> addTwo = x => x + 2; Func<Func<int, int>, Func<Func<int, int>, Func<int, int>>> o = new Func<Func<int, int>, Func<int, int>, Func<int, int>>(FuncExtensions.o).Curry(); Tuple<int> left1 = o.Tuple().Apply(addOne.Tuple()).Apply(addTwo.Tuple()).Apply(1.Tuple()); Tuple<int> right1 = addOne.Tuple().Apply(addTwo.Tuple().Apply(1.Tuple())); Assert.AreEqual(left1.Item1, right1.Item1); // f.Functor().Apply(a.Functor()) == f(a).Functor() Assert.AreEqual(addOne.Tuple().Apply(1.Tuple()).Item1, addOne(1).Tuple().Item1); // F.Apply(a.Functor()) == (f => f(a)).Functor().Apply(F) Tuple<int> left2 = addOne.Tuple().Apply(1.Tuple()); Tuple<int> right2 = new Func<Func<int, int>, int>(f => f(1)).Tuple().Apply(addOne.Tuple()); Assert.AreEqual(left2.Item1, right2.Item1); } [TestMethod()] public void HotTaskTest() { bool isExecuted1 = false; Func<int, int> addOne = x => { isExecuted1 = true; return x + 1; }; Task<Func<int, int>> hotAddOne = Task.Run(() => addOne); Task<int> hotTwo = Task.Run(() => 2); Task<int> query1 = hotAddOne.Apply(hotTwo); Assert.AreEqual(2 + 1, query1.Result); Assert.IsTrue(isExecuted1); // f.Functor().Apply(F) == F.Select(f) Assert.AreEqual(addOne.Task().Apply(1.Task()).Result, 1.Task().Select(addOne).Result); // id.Functor().Apply(F) == F Func<int, int> id = Functions.Id; Assert.AreEqual(id.Task().Apply(1.Task()).Result, 1.Task().Result); // o.Functor().Apply(F1).Apply(F2).Apply(F3) == F1.Apply(F2.Apply(F3)) Func<int, int> addTwo = x => x + 2; Func<Func<int, int>, Func<Func<int, int>, Func<int, int>>> o = new Func<Func<int, int>, Func<int, int>, Func<int, int>>(FuncExtensions.o).Curry(); Task<int> left1 = o.Task().Apply(addOne.Task()).Apply(addTwo.Task()).Apply(1.Task()); Task<int> right1 = addOne.Task().Apply(addTwo.Task().Apply(1.Task())); Assert.AreEqual(left1.Result, right1.Result); // f.Functor().Apply(a.Functor()) == f(a).Functor() Assert.AreEqual(addOne.Task().Apply(1.Task()).Result, addOne(1).Task().Result); // F.Apply(a.Functor()) == (f => f(a)).Functor().Apply(F) Task<int> left2 = addOne.Task().Apply(1.Task()); Task<int> right2 = new Func<Func<int, int>, int>(f => f(1)).Task().Apply(addOne.Task()); Assert.AreEqual(left2.Result, right2.Result); } [TestMethod()] public void ColdTaskTest() { bool isExecuted1 = false; bool isExecuted2 = false; Func<int, int> addOne = x => { isExecuted1 = true; return x + 1; }; Task<Func<int, int>> coldAddOne = new Task<Func<int, int>>(() => addOne); Task<int> coldTwo = new Task<int>(() => { isExecuted2 = true; return 2; }); Task<int> query2 = coldAddOne.Apply(coldTwo); Assert.IsFalse(isExecuted1); // Laziness. Assert.IsFalse(isExecuted2); // Laziness. coldAddOne.Start(); // Execution. coldTwo.Start(); // Execution. Assert.AreEqual(2 + 1, query2.Result); Assert.IsTrue(isExecuted1); Assert.IsTrue(isExecuted2); // f.Functor().Apply(F) == F.Select(f) coldAddOne = new Task<Func<int, int>>(() => addOne); coldTwo = new Task<int>(() => 2); Task<int> left = coldAddOne.Apply(coldTwo); Task<int> right = coldTwo.Select(addOne); coldAddOne.Start(); coldTwo.Start(); Assert.AreEqual(left.Result, right.Result); // id.Functor().Apply(F) == F Func<int, int> id = Functions.Id; Task<Func<int, int>> coldId = new Task<Func<int, int>>(() => id); coldTwo = new Task<int>(() => 2); left = coldId.Apply(coldTwo); right = coldTwo; coldId.Start(); coldTwo.Start(); Assert.AreEqual(left.Result, right.Result); // o.Functor().Apply(F1).Apply(F2).Apply(F3) == F1.Apply(F2.Apply(F3)) coldAddOne = new Task<Func<int, int>>(() => addOne); Func<int, int> addTwo = x => x + 2; Task<Func<int, int>> coldAddTwo = new Task<Func<int, int>>(() => addTwo); Func<Func<int, int>, Func<Func<int, int>, Func<int, int>>> o = new Func<Func<int, int>, Func<int, int>, Func<int, int>>(FuncExtensions.o).Curry(); Task<Func<Func<int, int>, Func<Func<int, int>, Func<int, int>>>> coldComposite = new Task<Func<Func<int, int>, Func<Func<int, int>, Func<int, int>>>>(() => o); coldTwo = new Task<int>(() => 2); left = coldComposite.Apply(coldAddOne).Apply(coldAddTwo).Apply(coldTwo); right = coldAddOne.Apply(coldAddTwo.Apply(coldTwo)); coldComposite.Start(); coldAddOne.Start(); coldAddTwo.Start(); coldTwo.Start(); Assert.AreEqual(left.Result, right.Result); // f.Functor().Apply(a.Functor()) == f(a).Functor() coldAddOne = new Task<Func<int, int>>(() => addOne); coldTwo = new Task<int>(() => 2); left = coldAddOne.Apply(coldTwo); right = new Task<int>(() => addOne(2)); coldAddOne.Start(); coldTwo.Start(); right.Start(); Assert.AreEqual(left.Result, right.Result); // F.Apply(a.Functor()) == (f => f(a)).Functor().Apply(F) coldAddOne = new Task<Func<int, int>>(() => addOne); coldTwo = new Task<int>(() => 2); left = coldAddOne.Apply(coldTwo); Task<Func<Func<int, int>, int>> coldApplyTwo = new Task<Func<Func<int, int>, int>>(() => f => f(2)); right = coldApplyTwo.Apply(coldAddOne); coldAddOne.Start(); coldTwo.Start(); coldApplyTwo.Start(); Assert.AreEqual(left.Result, right.Result); } }