Skip to content

Instantly share code, notes, and snippets.

@kamoshi
Created April 9, 2026 09:11
Show Gist options
  • Select an option

  • Save kamoshi/a0045c22952b98283137f573dfdd2c7e to your computer and use it in GitHub Desktop.

Select an option

Save kamoshi/a0045c22952b98283137f573dfdd2c7e to your computer and use it in GitHub Desktop.
C# 15 monoid
using HelloWorld;
// ── Maybe<T> usage ────────────────────────────────────────────────────────────
Maybe<string> name = "Alice";
Maybe<string> empty = new None();
Console.WriteLine($"Map: {name.Map(s => s.Length).Show()}");
Console.WriteLine($"Bind: {name.Bind(s => s.Length > 3
? (Maybe<string>)s.ToUpper()
: new None()).Show()}");
Console.WriteLine($"OrElse: {empty.GetOrElse("(nobody)")}");
// ── Monoid examples ───────────────────────────────────────────────────────────
Console.WriteLine();
var a = MaybeMonoid<int, SumMonoid>.Combine("hello".Length, 10);
var b = MaybeMonoid<int, SumMonoid>.Combine(new None(), 10);
var c = MaybeMonoid<int, SumMonoid>.Combine(new None(), new None());
Console.WriteLine($"Some(5) <> Some(10) = {a.Show()}");
Console.WriteLine($"None <> Some(10) = {b.Show()}");
Console.WriteLine($"None <> None = {c.Show()}");
// Fold a sequence
Console.WriteLine();
Maybe<int>[] values = [1, new None(), 3, new None(), 5];
Maybe<int> total = MonoidUtils<Maybe<int>, MaybeMonoid<int, SumMonoid>>.Concat(values);
Console.WriteLine($"Fold [1, None, 3, None, 5] = {total.Show()}");
namespace HelloWorld
{
// ── None ─────────────────────────────────────────────────────────────────
public record struct None;
// ── Maybe<T> union ───────────────────────────────────────────────────────
public union Maybe<T>(T, None);
public static class MaybeExtensions
{
public static string Show<T>(this Maybe<T> m) => m switch
{
T v => $"Some({v})",
null => "None",
_ => "None"
};
public static Maybe<TResult> Map<T, TResult>(
this Maybe<T> m, Func<T, TResult> f) => m switch
{
T v => f(v),
_ => new None()
};
public static Maybe<TResult> Bind<T, TResult>(
this Maybe<T> m, Func<T, Maybe<TResult>> f) => m switch
{
T v => f(v),
_ => new None()
};
public static T GetOrElse<T>(this Maybe<T> m, T fallback) => m switch
{
T v => v,
_ => fallback
};
}
// ── Monoid typeclass — interface so static abstract members work ───────────
// (static abstract is a C# 11 feature, only supported on interfaces)
public interface IMonoid<T>
{
static abstract T Identity { get; }
static abstract T Combine(T a, T b);
}
// ── Abstract class for derived utility — uses the interface as a constraint ─
public abstract class MonoidUtils<T, TMonoid> where TMonoid : IMonoid<T>
{
public static T Concat(IEnumerable<T> items) =>
items.Aggregate(TMonoid.Identity, TMonoid.Combine);
public static T Stimes(int n, T a)
{
var result = TMonoid.Identity;
for (var i = 0; i < n; i++) result = TMonoid.Combine(result, a);
return result;
}
}
// ── Concrete monoid: int addition ─────────────────────────────────────────
public class SumMonoid : IMonoid<int>
{
public static int Identity => 0;
public static int Combine(int a, int b) => a + b;
}
// ── Lifted Maybe monoid ───────────────────────────────────────────────────
// Laws hold when TMonoid satisfies monoid laws for T:
// Identity = None
// Combine(None, y) = y
// Combine(x, None) = x
// Combine(Some a, Some b) = Some(TMonoid.Combine(a, b))
public class MaybeMonoid<T, TMonoid> : IMonoid<Maybe<T>>
where TMonoid : IMonoid<T>
{
public static Maybe<T> Identity => new None();
public static Maybe<T> Combine(Maybe<T> a, Maybe<T> b) =>
(a, b) switch
{
(T va, T vb) => TMonoid.Combine(va, vb),
(T va, _) => va,
(_, T vb) => vb,
_ => new None()
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment