Teoría de categorías a través de C# (9) Bifunctor

Teoría de categorías a través de C# (9) Bifunctor

[LINQ a través de la serie C#]

[Teoría de categorías a través de la serie C#]

Última versión:https://weblogs.asp.net/dixin/category-theory-via-csharp-5-bifunctor

Bifunción

Como se discutió en todas las partes anteriores del funtor, un funtor es un envoltorio de un objeto con la capacidad de "Seleccionar" para preservar un morfismo en otro.

Un bifuntor, como su nombre lo indica, es un envoltorio de 2 objetos, con la capacidad de "Seleccionar" para conservar 2 morfismos en otro morfismo:

Como se representa en el diagrama anterior, F:

  • mapea objetos X ∈ ob(C), Y ∈ ob(D) a objetos F(X, Y) ∈ ob(E)
  • también mapea el morfismo mC :X → X’ ∈ hom(C), mD :Y → Y’ ∈ hom(D) a un nuevo morfismo mE :F(X, Y) → F(X’, Y’) ∈ hom(E)

y satisface las leyes de los funtores:

  1. Seleccionar(idX , idY ) ≌ idF(X, Y)
  2. Seleccionar(m2 ∘ m1 , n2 ∘ n1 ) ≌ Seleccionar(m2 , n2 ) ∘ F(m1 , n1 )

Recuerde la definición pseudo C# de funtor:

// Cannot be compiled.
public interface IFunctor<in TSourceCategory, out TTargetCategory, TFunctor<>>
    where TSourceCategory : ICategory<TSourceCategory>
    where TTargetCategory : ICategory<TTargetCategory>
    where TFunctor<> : IFunctor<TSourceCategory, TTargetCategory, TFunctor<>>
{
    IMorphism<TFunctor<TSource>, TFunctor<TResult>, TTargetCategory> Select<TSource, TResult>(
        IMorphism<TSource, TResult, TSourceCategory> selector);
}

Del mismo modo, bifunctor se puede definir como:

// Cannot be compiled
public interface IBinaryFunctor<in TSourceCategory1, in TSourceCategory2, out TTargetCategory, TBinaryFunctor< , >>
    where TSourceCategory1 : ICategory<TSourceCategory1>
    where TSourceCategory2 : ICategory<TSourceCategory2>
    where TTargetCategory : ICategory<TTargetCategory>
    where TBinaryFunctor< , > : IBinaryFunctor<TSourceCategory1, TSourceCategory2, TTargetCategory, TBinaryFunctor< , >>
{
    IMorphism<TBinaryFunctor<TSource1, TSource2>, TBinaryFunctor<TResult1, TResult2>, TTargetCategory> Select<TSource1, TSource2, TResult1, TResult2>(
        IMorphism<TSource1, TResult1, TSourceCategory1> selector1, IMorphism<TSource2, TResult2, TSourceCategory2> selector2);
}

Como se mencionó anteriormente, el bifunctor envuelve 2 objetos. Así que aquí TBinaryFunctor<,> toma 2 parámetros para que pueda envolver 2 tipos. Posteriormente, la función Seleccionar se implementará como método de extensión para cada bifuntor, de la misma manera que se manejan los funtores.

Tri-funtor y multi-funtor se pueden definir e implementar de manera similar.

Bifunción C#/.NET

Teóricamente, el bifuntor intuitivo es Tuple<,>. Sin embargo, como se mencionó en una parte anterior, Tuple<,> puede tener un comportamiento inesperado en el contexto de C#/LINQ, por lo que solo se considerará similar a un funtor. Entonces, para ser coherentes, Tuple<> o Tuple<,>, … solo se usarán como utilidades en la teoría de categorías a través de publicaciones de C#, en lugar de como funtor o bifuntor. Aquí hay un escenario para Tuple<,>, por lo que se puede crear su versión perezosa Lazy<,>:

public class Lazy<T1, T2>
{
    private readonly Lazy<Tuple<T1, T2>> lazy;

    public Lazy(Func<T1> factory1, Func<T2> factory2)
        : this(() => Tuple.Create(factory1(), factory2()))
    {
    }

    public Lazy(T1 value1, T2 value2)
        : this(() => Tuple.Create(value1, value2))
    {
    }

    public Lazy(Func<Tuple<T1, T2>> factory)
    {
        this.lazy = new Lazy<Tuple<T1, T2>>(factory);
    }

    public T1 Value1
    {
        [Pure]get { return this.lazy.Value.Item1; }
    }

    public T2 Value2
    {
        [Pure]get { return this.lazy.Value.Item2; }
    }
}

La diferencia con el funtor Lazy<> es, como dice la definición, Lazy<,> envuelve 2 tipos de valores.

Para hacer que Lazy<,> sea un bifuntor, simplemente cree estos métodos de extensión bi-Select (en Haskell esto se llama bimap):

// [Pure]
public static partial class LazyExtensions
{
    public static Lazy<TResult1, TResult2> Select<TSource1, TSource2, TResult1, TResult2>
        (this Lazy<TSource1, TSource2> source, 
            Func<TSource1, TResult1> selector1, 
            Func<TSource2, TResult2> selector2) =>
                new Lazy<TResult1, TResult2>(() => selector1(source.Value1), () => selector2(source.Value2));

    public static IMorphism<Lazy<TSource1, TSource2>, Lazy<TResult1, TResult2>, DotNet> Select<TSource1, TSource2, TResult1, TResult2>
        (IMorphism<TSource1, TResult1, DotNet> selector1, IMorphism<TSource2, TResult2, DotNet> selector2) => 
            new DotNetMorphism<Lazy<TSource1, TSource2>, Lazy<TResult1, TResult2>>(
                source => source.Select(selector1.Invoke, selector2.Invoke));
}

La diferencia con el funtor Lazy<> es que hay 2 selectores, un selector para cada tipo envuelto.

Pruebas unitarias

La siguiente prueba unitaria demuestra el uso y la pereza de Lazy<,>:

[TestClass()]
public class BinaryFunctorTests
{
    [TestMethod()]
    public void LazyTest()
    {
        bool isExecuted1 = false;
        bool isExecuted2 = false;
        Lazy<int, string> lazyBinaryFunctor = new Lazy<int, string>(1, "abc");
        Func<int, bool> selector1 = x => { isExecuted1= true; return x > 0; };
        Func<string, int> selector2 = x => { isExecuted2 = true; return x.Length; };

        Lazy<bool, int> query = lazyBinaryFunctor.Select(selector1, selector2);
        Assert.IsFalse(isExecuted1); // Laziness.
        Assert.IsFalse(isExecuted2); // Laziness.

        Assert.AreEqual(true, query.Value1); // Execution.
        Assert.AreEqual("abc".Length, query.Value2); // Execution.
        Assert.IsTrue(isExecuted1);
        Assert.IsTrue(isExecuted2); 
    }
}

Tenga en cuenta que Tuple<,> no es tan perezoso.