990 words
5 minutes
Category Theory via C# (5) Bifunctor
TIP

Functional Programming and LINQ via C# Series

Category Theory via C# Series

Bifunctor#

A functor is the mapping from 1 object to another object, with a “Select” ability to map 1 morphism to another morphism. A bifunctor (binary functor), as the name implies, is the mapping from 2 objects and from 2 morphisms. Giving category C, D and E, bifunctor F from category C, D to E is a structure-preserving morphism from C, D to E, denoted F: C × D → E:

image

  • F maps objects X ∈ ob(C), Y ∈ ob(D) to object F(X, Y) ∈ ob(E)
  • F also maps morphisms mC:XXhom(C)m_C: X → X’ ∈ hom(C), mD:YYhom(D)m_D: Y → Y’ ∈ hom(D) to morphism mE:F(X,Y)F(X,Y)hom(E)m_E: F(X, Y) → F(X’, Y’) ∈ hom(E)
flowchart TB
    subgraph C["C"]
        X{"X"}
        XP{"X'"}

        X-->|"m<sub>C</sub>"|XP
    end

    subgraph D["D"]
        Y{"Y"}
        YP{"YP"}

        Y-->|"m<sub>D</sub>"|YP
    end

    subgraph E["E"]
        FXY{"F(X, Y)"}
        FXPYP{"F(X', Y')"}

        FXY-->|"m<sub>E</sub> <br /> Select(m<sub>C</sub>, m<sub>D</sub>)"|FXPYP
    end

    X---Y-->FXY
    XP---YP-->FXPYP

    style C stroke:#6666ff
    style D stroke:#ff6666
    style E stroke:#66ff66

    style X stroke:#6666ff
    style XP stroke:#6666ff

    style Y stroke:#ff6666
    style YP stroke:#ff6666

    style FXY stroke:#66ff66
    style FXPYP stroke:#66ff66

In DotNet category, bifunctors are binary endofunctors, and can be defined as:

// Cannot be compiled.
public interface IBifunctor<TBifunctor<,>> where TBifunctor<,> : IBifunctor<TBifunctor<,>>
{
Func<TBifunctor<TSource1, TSource2>, TBifunctor<TResult1, TResult2>> Select<TSource1, TSource2, TResult1, TResult2>(
Func<TSource1, TResult1> selector1, Func<TSource2, TResult2> selector2);
}

The most intuitive built-in bifunctor is ValueTuple<,>. Apparently ValueTuple<,> can be viewed as a type constructor of kind * –> * –> *, which accepts 2 concrete types to and return another concrete type. Its Select implementation is also straightforward:

public static partial class ValueTupleExtensions // ValueTuple<T1, T2> : IBifunctor<ValueTuple<,>>
{
// Bifunctor Select: (TSource1 -> TResult1, TSource2 -> TResult2) -> (ValueTuple<TSource1, TSource2> -> ValueTuple<TResult1, TResult2>).
public static Func<ValueTuple<TSource1, TSource2>, ValueTuple<TResult1, TResult2>> Select<TSource1, TSource2, TResult1, TResult2>(
Func<TSource1, TResult1> selector1, Func<TSource2, TResult2> selector2) => source =>
Select(source, selector1, selector2);
// LINQ-like Select: (ValueTuple<TSource1, TSource2>, TSource1 -> TResult1, TSource2 -> TResult2) -> ValueTuple<TResult1, TResult2>).
public static ValueTuple<TResult1, TResult2> Select<TSource1, TSource2, TResult1, TResult2>(
this ValueTuple<TSource1, TSource2> source,
Func<TSource1, TResult1> selector1,
Func<TSource2, TResult2> selector2) =>
(selector1(source.Item1), selector2(source.Item2));
}

However, similar to ValueTuple<> functor’s Select method, ValueTuple<,> bifunctor’s Select method has to call selector1 and selector2 immediately. To implement deferred execution, the following Lazy<,> bifunctor can be defined:

public class Lazy<T1, T2>
{
private readonly Lazy<(T1, T2)> lazy;
public Lazy(Func<(T1, T2)> factory) => this.lazy = new Lazy<(T1, T2)>(factory);
public T1 Value1 => this.lazy.Value.Item1;
public T2 Value2 => this.lazy.Value.Item2;
public override string ToString() => this.lazy.Value.ToString();
}

Lazy<,> is simply the lazy version of ValueTuple<,>. Jut like Lazy<>, Lazy<,> can be constructed with a factory function, so that the call to selector1 and selector2 are deferred:

public static partial class LazyExtensions // Lazy<T1, T2> : IBifunctor<Lazy<,>>
{
// Bifunctor Select: (TSource1 -> TResult1, TSource2 -> TResult2) -> (Lazy<TSource1, TSource2> -> Lazy<TResult1, TResult2>).
public static Func<Lazy<TSource1, TSource2>, Lazy<TResult1, TResult2>> Select<TSource1, TSource2, TResult1, TResult2>(
Func<TSource1, TResult1> selector1, Func<TSource2, TResult2> selector2) => source =>
Select(source, selector1, selector2);
// LINQ-like Select: (Lazy<TSource1, TSource2>, TSource1 -> TResult1, TSource2 -> TResult2) -> Lazy<TResult1, TResult2>).
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)));
}

Monoidal category#

With the help of bifunctor, monoidal category can be defined. A monoidal category is a category C equipped with:

  • A bifunctor ⊗ as the monoid binary multiplication operation: bifunctor ⊗ maps 2 objects in C to another object in C, denoted C ⊗ C → C, which is also called the monoidal product or tensor product.
  • An unit object I ∈ ob(C) as the monoid unit, also called tensor unit

For (C, ⊗, I) to be a monoid, it also needs to be equipped with the following natural transformations, so that the monoid laws are satisfied:

  • Associator αX,Y,Z:(XY)ZX(YZ)α_{X, Y, Z}: (X ⊗ Y) ⊗ Z ⇒ X ⊗ (Y ⊗ Z) for the associativity law, where X, Y, Z ∈ ob(C)
  • Left unitor λX:IXXλ_X: I ⊗ X ⇒ X for the left unit law, and right unitor ρX:XIXρ_X: X ⊗ I ⇒ X for the right unit law, where X ∈ ob(C)

The following monoid triangle identity and pentagon identity diagrams still commute for monoidal category:

image_thumb12_thumb

flowchart TD
    subgraph X[" "]
        A{"(X ⊗ I) ⊗ Y"}
        B{"X ⊗ (I ⊗ Y)"}
    end

    subgraph Y[" "]
        C{"X ⊗ Y"}
    end

    A-->|"α<sub>X, I, Y</sub>"|B
    A-->|"ρ<sub>X</sub> ⊗ id<sub>Y</sub>"|C
    B-->|"id<sub>X</sub> ⊗ λ<sub>Y</sub>"|C

    style X stroke-width:0px
    style Y stroke-width:0px

    style A stroke:#6666ff
    style B stroke:#ff6666
    style C stroke:#66ff66

Untitled-2.fw_thumb_thumb

flowchart LR
    subgraph X[" "]
        A{"(W ⊗ X) ⊗ (Y ⊗ Z)"}
    end

    subgraph Y[" "]
        B{"((W ⊗ X) ⊗ Y) ⊗ Z"}
        C{"W ⊗ (X ⊗ (Y ⊗ Z))"}
    end

    subgraph Z[" "]
        D{"(W ⊗ (X ⊗ Y)) ⊗ Z"}
        E{"W ⊗ ((X ⊗ Y) ⊗ Z)"}
    end
    

    B-->|"α<sub>W ⊗ X, Y, Z</sub>"|A
    A-->|"α<sub>W, X, Y ⊗ Z</sub>"|C
    B-->|"α<sub>W, X, Y</sub> ⊗ id<sub>Z</sub>"|D
    C-->|"id<sub>W</sub> ⊗ α<sub>X, Y, Z</sub>"|E
    D-->|"α<sub>W, X ⊗ Y, Z</sub>"|E

    style X stroke-width:0px
    style Y stroke-width:0px
    style Z stroke-width:0px

    style A stroke:#6666ff
    style B stroke:#ff6666
    style C stroke:#66ff66
    style D stroke:#ffff66
    style E stroke:#66ffff

Here for monoidal category, the above ⊙ (general multiplication operator) becomes ⊗ (bifunctor).

Monoidal category can be simply defined as:

public interface IMonoidalCategory<TObject, TMorphism> : ICategory<TObject, TMorphism>, IMonoid<TObject> { }

DotNet category is monoidal category, with the most intuitive bifunctor ValueTuple<,> as the monoid multiplication, and Unit type as the monoid unit:

public partial class DotNetCategory : IMonoidalCategory<Type, Delegate>
{
public static Type Multiply(Type value1, Type value2) => typeof(ValueTuple<,>).MakeGenericType(value1, value2);
public static Type Unit => typeof(Unit);
}

To have (DotNet, ValueTuple<,>, Unit) satisfy the monoid laws, the associator, left unitor and right unitor are easy to implement:

public partial class DotNetCategory
{
// Associator: (T1 x T2) x T3 -> T1 x (T2 x T3)
// Associator: ValueTuple<ValueTuple<T1, T2>, T3> -> ValueTuple<T1, ValueTuple<T2, T3>>
public static (T1, (T2, T3)) Associator<T1, T2, T3>(((T1, T2), T3) product) =>
(product.Item1.Item1, (product.Item1.Item2, product.Item2));
// LeftUnitor: Unit x T -> T
// LeftUnitor: ValueTuple<Unit, T> -> T
public static T LeftUnitor<T>((Unit, T) product) => product.Item2;
// RightUnitor: T x Unit -> T
// RightUnitor: ValueTuple<T, Unit> -> T
public static T RightUnitor<T>((T, Unit) product) => product.Item1;
}
Category Theory via C# (5) Bifunctor
https://codingonwheels.com/posts/category-theory-via-csharp-5-bifunctor/
Author
Dixin
Published at
2025-12-15
License
CC BY-NC-SA 4.0