Skip to content

Latest commit

 

History

History
1544 lines (1267 loc) · 53.1 KB

2019-11-08.md

File metadata and controls

1544 lines (1267 loc) · 53.1 KB

Date: November 8, 2019
Original Post: https://gist.github.com/ZacharyPatten/bdd44cae81155484e6ab5b7555390003

Discussion: Static Generic Argument Expressions And Method Constraints

This was a work in progress for a post on dotnet/csharplang: dotnet/csharplang#2904

EDIT: I originally thought this would only work with static (non capturing) expressions, but it should work with captures too, so I crosses out "static".

Overview

I think it is possible to allow static expressions and static methods to be used as generic arguments in C#. This would allow for more optimized functional programming without having to wrap functions inside custom struct types. Here is an example:

Overview Code Snippet 1 [click to expand]

using System;

namespace Example
{
	public static class Program
	{
		public static void Main()
		{
			PerformAction<int, { a => Console.Write(a) }>(7);
		}

		public static void PerformAction<T, Action>(T a)
			where Action : [void method(T)]
		{
			Action(a);
		}
	}
}

The { a => Console.Write(a) } expression in the above code snippet could be compiled into a seperate static method like so:

Overview Code Snippet 2 [click to expand]

using System;

namespace Example
{
	public static class Program
	{
		public static void Main()
		{
			PerformAction<int, ConsoleWrite>(7);
		}

		public static void ConsoleWrite(int a) => Console.Write(a);

		public static void PerformAction<T, Action>(T a)
			where Action : [void method(T)]
		{
			Action(a);
		}
	}
}

From there, the ConsoleWrite method could be wrapped inside a compiler generated struct that would be used as the final generic parameter:

Overview Code Snippet 3 [click to expand]

using System;

namespace Example
{
	public static class Program
	{
		public static void Main()
		{
			PerformAction<int, g__ConsoleWrite_1>(7);
		}

		// IAction<A> (and similar interface types) would
		// need to be added to the .NET libraries
		public interface IAction<A>
		{
			void Do(A a);
		}

		public struct g__ConsoleWrite_1 : IAction<int>
		{
			public void Do(int a) => ConsoleWrite(a);
		}

		public static void ConsoleWrite(int a) => Console.Write(a);

		public static void PerformAction<T, Action>(T a)
			where Action : struct, IAction<T>
		{
			Action action = default;
			action.Do(a);
		}
	}
}

Aside from syntax changes, there would also need to be additional interface types IAction<T>, IFunction<T>, IAction1Ref<T>, IAction<T, T>, IFunction<T, T>, etc. for the mapping between the generic parameter and the compiler generated struct.

EDIT: Rather than adding interfaces, it might be possible/better for the compiler to generate both the interfaces and the structs.

Motivation

This is more optimized that the current standard delegate syntax if the function is static, because it would not result in a delegate type at runtime. It would be resolved at compile time and potentially inlined by the JIT. Since no delegate would exist at runtime, there would be no heap allocation for it. Here are some testing results using Benchmark.NET:

Benchmark 1 Code [click to expand]

using System;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace Benchmarks
{
	public class Benchmarks
	{
		int temp;
		int _1;
		int _2;
		int _3;
		int _4;
		int _5;

		[GlobalSetup]
		public void Setup()
		{
			_1 = 1;
			_2 = 2;
			_3 = 3;
			_4 = 4;
			_5 = 5;
		}

		[Benchmark]
		public void IntegerAdd()
		{
			temp = _1 + _1;
			temp = _2 + _2;
			temp = _3 + _3;
			temp = _4 + _4;
			temp = _5 + _5;
		}

		[Benchmark]
		public void IntegerAddStructWithAggressiveInlining()
		{
			temp = DoWithAggressiveInlining<int, g__AdditionWithAggressiveInlining_1>(_1, _1);
			temp = DoWithAggressiveInlining<int, g__AdditionWithAggressiveInlining_1>(_2, _2);
			temp = DoWithAggressiveInlining<int, g__AdditionWithAggressiveInlining_1>(_3, _3);
			temp = DoWithAggressiveInlining<int, g__AdditionWithAggressiveInlining_1>(_4, _4);
			temp = DoWithAggressiveInlining<int, g__AdditionWithAggressiveInlining_1>(_5, _5);
		}

		[Benchmark]
		public void IntegerAddLinqWithAggressiveInlining()
		{
			temp = AdditionWithAggressiveInlining(_1, _1);
			temp = AdditionWithAggressiveInlining(_2, _2);
			temp = AdditionWithAggressiveInlining(_3, _3);
			temp = AdditionWithAggressiveInlining(_4, _4);
			temp = AdditionWithAggressiveInlining(_5, _5);
		}

		[Benchmark]
		public void IntegerAddStruct()
		{
			temp = Do<int, g__Addition_1>(_1, _1);
			temp = Do<int, g__Addition_1>(_2, _2);
			temp = Do<int, g__Addition_1>(_3, _3);
			temp = Do<int, g__Addition_1>(_4, _4);
			temp = Do<int, g__Addition_1>(_5, _5);
		}

		[Benchmark]
		public void IntegerAddLinq()
		{
			temp = Addition(_1, _1);
			temp = Addition(_2, _2);
			temp = Addition(_3, _3);
			temp = Addition(_4, _4);
			temp = Addition(_5, _5);
		}

		public struct g__AdditionWithAggressiveInlining_1 : IFunction<int, int, int>
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			public int Do(int a, int b) => a + b;
		}

		public struct g__Addition_1 : IFunction<int, int, int>
		{
			public int Do(int a, int b) => a + b;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static T DoWithAggressiveInlining<T, Addition>(T a, T b)
			where Addition : struct, IFunction<T, T, T>
		{
			Addition addition = default;
			return addition.Do(a, b);
		}

		public static T Do<T, Addition>(T a, T b)
			where Addition : struct, IFunction<T, T, T>
		{
			Addition addition = default;
			return addition.Do(a, b);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static T AdditionWithAggressiveInlining<T>(T a, T b)
		{
			return AdditionImplementation<T>.Function(a, b);
		}

		public static T Addition<T>(T a, T b)
		{
			return AdditionImplementation<T>.Function(a, b);
		}

		internal static class AdditionImplementation<T>
		{
			internal static Func<T, T, T> Function = (T a, T b) =>
			{
				ParameterExpression A = Expression.Parameter(typeof(T));
				ParameterExpression B = Expression.Parameter(typeof(T));
				Expression BODY = Expression.Add(A, B);
				Function = Expression.Lambda<Func<T, T, T>>(BODY, A, B).Compile();
				return Function(a, b);
			};
		}
	}

	public interface IFunction<A, B, C> { C Do(A a, B b); }

	public class Program
	{
		public static void Main(string[] args)
		{
			var summary = BenchmarkRunner.Run<Benchmarks>();
		}
	}
}

Benchmark 1 Results [click to expand]

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i7-4790K CPU 4.00GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

Method Mean Error StdDev
IntegerAdd 0.6769 ns 0.0183 ns 0.0171 ns
IntegerAddStructWithAggressiveInlining 0.9609 ns 0.0453 ns 0.0424 ns
IntegerAddLinqWithAggressiveInlining 10.3589 ns 0.1538 ns 0.1438 ns
IntegerAddStruct 6.5660 ns 0.1569 ns 0.1468 ns
IntegerAddLinq 10.9472 ns 0.1388 ns 0.1298 ns

Benchmark 2 Code [click to expand]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace Benchmarks
{
	public class Benchmarks
	{
		private static int[] data;
		private static int sum;

		[GlobalSetup]
		public void Setup()
		{
			const int N = 10;
			int[] dataArray = new int[N];
			for (int i = 0; i < N; i++)
			{
				dataArray[i] = i;
			}
			data = dataArray;
		}

		[Benchmark]
		public void Control()
		{
			sum = 0;
			foreach (int i in data)
			{
				if (i % 2 == 0)
				{
					sum += i;
				}
			}
		}

		[Benchmark]
		public void LinqWhereLambda()
		{
			sum = 0;
			foreach (int i in data.Where(i => i % 2 == 0))
			{
				sum += i;
			}
		}

		[Benchmark]
		public void LinqWhere()
		{
			sum = 0;
			foreach (int i in data.Where(IsEven))
			{
				sum += i;
			}
		}

		[Benchmark]
		public void CustomWhereLambda()
		{
			sum = 0;
			foreach (int i in data.CustomWhere(i => i % 2 == 0))
			{
				sum += i;
			}
		}

		[Benchmark]
		public void CustomWhere()
		{
			sum = 0;
			foreach (int i in data.CustomWhere(IsEven))
			{
				sum += i;
			}
		}

		[Benchmark]
		public void GenericStructWhere()
		{
			sum = 0;
			foreach (int i in data.Where<int, g__IsEven_1>())
			{
				sum += i;
			}
		}

		[Benchmark]
		public void GenericStructWhereAction()
		{
			sum = 0;
			data.Where<int, g__IsEven_1, g__Action_1>();
		}

		[Benchmark]
		public void CustomWhereWithAggressiveInlining()
		{
			sum = 0;
			foreach (int i in data.CustomWhere(IsEvenWithAggressiveInlining))
			{
				sum += i;
			}
		}

		[Benchmark]
		public void GenericStructWhereWithAggressiveInlining()
		{
			sum = 0;
			foreach (int i in data.Where<int, g__IsEvenWithAggressiveInlining_1>())
			{
				sum += i;
			}
		}

		[Benchmark]
		public void GenericStructWhereActionWithAggressiveInlining()
		{
			sum = 0;
			data.Where<int, g__IsEvenWithAggressiveInlining_1, g__ActionWithAggressiveInlining_1>();
		}

		public struct g__ActionWithAggressiveInlining_1 : IAction<int>
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
			public void Do(int a) => sum += a;
		}

		public struct g__IsEvenWithAggressiveInlining_1 : IFunction<int, bool>
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
			public bool Do(int a) => IsEvenWithAggressiveInlining(a);
		}

		public struct g__Action_1 : IAction<int>
		{
			public void Do(int a) => sum += a;
		}

		public struct g__IsEven_1 : IFunction<int, bool>
		{
			public bool Do(int a) => IsEven(a);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
		public static bool IsEvenWithAggressiveInlining(int i) => i % 2 == 0;

		public static bool IsEven(int i) => i % 2 == 0;

	}

	#region Interface Types That Would Be Added To .NET

	public interface IAction<A> { void Do(A a); }
	public interface IFunction<A, B> { B Do(A a); }

	#endregion

	public static class Extensions
	{
		public static IEnumerable<T> CustomWhere<T>(this T[] ienumerable, Predicate<T> where)
		{
			foreach (T value in ienumerable)
			{
				if (where(value))
				{
					yield return value;
				}
			}
		}

		public static IEnumerable<T> Where<T, Predicate>(this T[] ienumerable)
			/// There should be syntax sugar on the constraints to
			/// make them look more like function constraints.
			where Predicate : struct, IFunction<T, bool> // -> where Predicate : [bool method(T)]
		{
			#region Code that should be unnecessary

			Predicate predicate = default;

			#endregion

			/// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

			foreach (T value in ienumerable)
			{
				if (predicate.Do(value))
				{
					yield return value;
				}
			}
		}

		public static void Where<T, Predicate, Action>(this T[] ienumerable)
			/// There should be syntax sugar on the constraints to
			/// make them look more like function constraints.
			where Predicate : struct, IFunction<T, bool> // -> where Predicate : [bool method(T)]
			where Action : struct, IAction<T>            // -> where Action    : [void method(T)]
		{
			#region Code that should be unnecessary

			Predicate predicate = default;
			Action action = default;

			#endregion

			/// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

			foreach (T value in ienumerable)
			{
				if (predicate.Do(value))
				{
					action.Do(value);
				}
			}
		}
	}

	public class Program
	{
		public static void Main(string[] args)
		{
			var summary = BenchmarkRunner.Run<Benchmarks>();
		}
	}
}

Benchmark 2 Results [click to expand]

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i7-4790K CPU 4.00GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

Method Mean Error StdDev
Control 10.001 ns 0.1150 ns 0.1076 ns
LinqWhereLambda 71.749 ns 1.2415 ns 1.1613 ns
LinqWhere 82.121 ns 1.4050 ns 1.2455 ns
CustomWhereLambda 81.201 ns 1.2469 ns 1.0412 ns
CustomWhere 91.495 ns 1.7966 ns 2.0690 ns
GenericStructWhere 58.712 ns 0.5980 ns 0.5301 ns
GenericStructWhereAction 8.247 ns 0.0395 ns 0.0350 ns
CustomWhereWithAggressiveInlining 88.845 ns 1.1319 ns 1.0588 ns
GenericStructWhereWithAggressiveInlining 57.707 ns 0.2212 ns 0.2069 ns
GenericStructWhereActionWithAggressiveInlining 8.206 ns 0.0193 ns 0.0180 ns

Examples

Here are code exampes of this feature. These code examples all compile in current C# 8.0, but it results in ugly code. I have added comments to demonstrate what the syntax could look like if this feature was added.

Code Examples [click to expand]

using System;
using System.Collections.Generic;
using IntArrayEnumerator = System.ValueTuple<int, int[]>;

namespace Example
{
    public class Program
    {
        public static void Main()
        {
            // These are examples of "compile time delegates". Currently
            // you have to wrap methods inside of a struct that implements an
            // interface. Theoretically the C# language could be modified to allow
            // you to pass in functions via generic arguments so that you do not
            // have to wrap methods inside a custom struct.

            // Why? Delegates have the overhead of being invoked at runtime. They
            // are not inlined/resolved at compile time. Using struct-wrapped-functions
            // as generic parameters means the function is resolved/in-lined by the
            // end of the JIT.

            Console.WriteLine("IterationExample...");
            IterationExample.Run();

            Console.WriteLine("SumAndProductExample...");
            SumAndProductExample.Run();

            Console.WriteLine("FactorialExample...");
            FactorialExample.Run();

            Console.WriteLine("BinomeialCoefficientExample...");
            BinomeialCoefficientExample.Run();

            Console.WriteLine("IEnumerableExample...");
            IEnumerableExample.Run();

            Console.WriteLine("GraphSearchExample...");
            GraphSearchExample.Run();
        }
    }

    #region Interface Types That Would Be Added To .NET

    // These types would be new types in the .NET libraries

    public interface IAction { void Do(); }
    public interface IAction<A> { void Do(A a); }
    public interface IAction<A, B> { void Do(A a, B b); }
    public interface IFunction<A> { A Do(); }
    public interface IFunction<A, B> { B Do(A a); }
    public interface IFunction<A, B, C> { C Do(A a, B b); }
    public interface IAction1Ref<A> { void Do(ref A a); }
    public interface IAction1Out<A> { void Do(out A a); }
    public interface IAction2Out<A, B> { void Do(out A a, out B b); }
    public interface IAction1In1Out<A, B> { void Do(A a, out B b); }
    public interface IAction1In2Out<A, B, C> { void Do(A a, out B b, out C c); }
    public interface IFunction1Out<A, B> { B Do(out A a); }
    public interface IFunction2Out<A, B, C> { C Do(out A a, out B b); }
    public interface IFunction1Ref<A, B> { B Do(ref A a); }
    public interface IFunction1In1Ref<A, B, C> { C Do(A a, ref B b); }

    // there would need to be more... I just stopped here

    #endregion

    #region IterationExample

    public static class IterationExample
    {
        public static void Run()
        {
            int[] a = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, };

            /// Instead of passing in g__ConsoleWrite_1 you should be able to
            /// pass in ConsoleWrite as the generic argument and g__ConsoleWrite_1
            /// would be generated at compile time.

            a.ForEach<int, g__ConsoleWrite_1>(); // -> a.ForEach<int, ConsoleWrite>();
            Console.WriteLine();

            a.ForEach<int, g__ConsoleWrite_1, g__IsEven_1>(); // -> a.ForEach<int, ConsoleWrite, IsEven>();
            Console.WriteLine();

            /// The language could also allow for static, compile-time
            /// expressions to be used as generic arguments. Then you could
            /// write something similar to the following. Capture variables
            /// would not be supported as it would need to generate a method
            /// at compile time. This would compile to a seperate static
            /// method (NOT a delegate) that would be wrapped with a compiler
            /// generated struct, and the struct would be used as the generic
            /// parameter.

            //a.ForEach<int, { i => Console.Write(i) }> ();
            //Console.WriteLine();
            //a.ForEach<int, { i => Console.Write(i) }, { i => i % 2 == 0 }>();
            //Console.WriteLine();
        }

        #region Types that could be generated at compile time

        public struct g__ConsoleWrite_1 : IAction<int>
        {
            public void Do(int i) => ConsoleWrite(i);
        }

        public struct g__IsEven_1 : IFunction<int, bool>
        {
            public bool Do(int i) => IsEven(i);
        }

        #endregion

        public static void ConsoleWrite(int i) => Console.Write(i);
        public static bool IsEven(int i) => i % 2 == 0;

        public static void ForEach<T, Action>(this T[] array)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where Action : struct, IAction<T> // -> where [void Action(T)]
        {
            #region Code that should be unnecessary

            Action action = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            for (int i = 0; i < array.Length; i++) // -> for (int i = 0; i < array.Length; i++)
            {                                      // -> {
                action.Do(array[i]);               // ->     Action(array[i]);
            }                                      // -> }
        }

        public static void ForEach<T, Action, Where>(this T[] array)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where Action : struct, IAction<T>        // -> where Action : [void method(T)]
            where Where : struct, IFunction<T, bool> // -> where Where  : [bool method(T)]
        {
            #region Code that should be unnecessary

            Action action = default;
            Where where = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            for (int i = 0; i < array.Length; i++) // -> for (int i = 0; i < array.Length; i++)
            {                                      // -> {
                if (where.Do(array[i]))            // ->     if (Where(array[i]))
                {                                  // ->     {
                    action.Do(array[i]);           // ->         Action(array[i]);
                }                                  // ->     }
            }                                      // -> }
        }
    }

    #endregion

    #region SumAndProductExample

    public static class SumAndProductExample
    {
        public static void Run()
        {
            int[] a = new int[] { 7, 7, 8, 8, 9, 9, 10, };
            float[] b = new float[] { 7, 7, 8, 8, 9, 9, 10, };

            /// Instead of passing in g__Addition_1 you should be able to
            /// pass in Addition as the generic argument and g__Addition_1
            /// would be generated at compile time.

            Console.WriteLine(Compute<int, g__Addition_1>(a));         // -> Console.WriteLine(Compute<int, Addition>(a));
            Console.WriteLine(Compute<float, g__Addition_2>(b));       // -> Console.WriteLine(Compute<float, Addition>(a));
            Console.WriteLine(Compute<int, g__Multiplication_1>(a));   // -> Console.WriteLine(Compute<int, Multiplication>(b));
            Console.WriteLine(Compute<float, g__Multiplication_2>(b)); // -> Console.WriteLine(Compute<float, Multiplication>(b));

            /// The language could also allow for static, compile-time
            /// expressions to be used as generic arguments. Then you could
            /// write something similar to the following. Capture variables
            /// would not be supported as it would need to generate a method
            /// at compile time. This would compile to a seperate static
            /// method (NOT a delegate) that would be wrapped with a compiler
            /// generated struct, and the struct would be used as the generic
            /// parameter.

            //Console.WriteLine(Compute<int,   { (a, b) => a + b }>(a));
            //Console.WriteLine(Compute<float, { (a, b) => a + b }>(b));
            //Console.WriteLine(Compute<int,   { (a, b) => a * b }>(a));
            //Console.WriteLine(Compute<float, { (a, b) => a * b }>(b));
        }

        #region Types that could be generated at compile time

        public struct g__Addition_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Addition(a, b);
        }

        public struct g__Addition_2 : IFunction<float, float, float>
        {
            public float Do(float a, float b) => Addition(a, b);
        }

        public struct g__Multiplication_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Multiplication(a, b);
        }

        public struct g__Multiplication_2 : IFunction<float, float, float>
        {
            public float Do(float a, float b) => Multiplication(a, b);
        }

        #endregion

        // These functions should be able to be static local fuctions. Then
        // all the functionality could be inside the Main method rather
        // than seperated into seperate methods.
        public static int Addition(int a, int b) => a + b;
        public static float Addition(float a, float b) => a + b;
        public static int Multiplication(int a, int b) => a * b;
        public static float Multiplication(float a, float b) => a * b;

        public static T Compute<T, Binary>(params T[] array)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where Binary : struct, IFunction<T, T, T> // -> where Binary : [T method(T, T)]
        {
            #region Code that should be unnecessary

            Binary binary = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            T result = array[0];                      // -> T result = array[0];
            for (int i = 1; i < array.Length; i++)    // -> for (int i = 1; i < array.Length; i++)
            {                                         // -> {
                result = binary.Do(result, array[i]); // ->     result = Binary(result, array[i]);
            }                                         // -> }
            return result;                            // -> return result;
        }
    }

    #endregion

    #region FactorialExample

    public static class FactorialExample
    {
        public static void Run()
        {
            /// Instead of passing in g__Decrement_1 you should be able to
            /// pass in Decrement as the generic argument and g__Decrement_1
            /// would be generated at compile time.

            int factorialInt7 =      // -> int factorialInt7 =
                Factorial<           // ->     Factorial<
                    int,             // ->         int,
                    g__Decrement_1,  // ->         Decrement,
                    g__IsInteger_1,  // ->         IsInteger,
                    g__IsPositive_1, // ->         IsPositive,
                    g__Multiply_1    // ->         Multiply
                    >(7);            // ->         >(7);

            Console.WriteLine(factorialInt7);

            float factorialFloat7 =  // -> float factorialFloat7 =
                Factorial<           // ->     Factorial<
                    float,           // ->         float,
                    g__Decrement_2,  // ->         Decrement,
                    g__IsInteger_2,  // ->         IsInteger,
                    g__IsPositive_2, // ->         IsPositive,
                    g__Multiply_2    // ->         Multiply
                    >(7f);           // ->         >(7f);

            Console.WriteLine(factorialFloat7);

            /// The language could also allow for static, compile-time
            /// expressions to be used as generic arguments. Then you could
            /// write something similar to the following. Capture variables
            /// would not be supported as it would need to generate a method
            /// at compile time. This would compile to a seperate static
            /// method (NOT a delegate) that would be wrapped with a compiler
            /// generated struct, and the struct would be used as the generic
            /// parameter.

            //float factorialInt7 =
            //    Factorial <
            //        int,
            //        { (ref a) => --a },
            //        { a => true },
            //        { a => a > 0 },
            //        { (a, b) => a * b }
            //        > (7f);
            //
            //Console.WriteLine(factorialInt7);
            //
            //float factorialFloat7 =
            //    Factorial<
            //        float,
            //        { (ref a) => --a },
            //        { a => true },
            //        { a => a > 0 },
            //        { (a, b) => a * b }
            //        >(7f);
            //
            //Console.WriteLine(factorialFloat7);
        }

        #region Types that should be generated at compile time

        public struct g__Multiply_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Multiply(a, b);
        }

        public struct g__Multiply_2 : IFunction<float, float, float>
        {
            public float Do(float a, float b) => Multiply(a, b);
        }

        public struct g__Decrement_1 : IAction1Ref<int>
        {
            public void Do(ref int a) => Decrement(ref a);
        }

        public struct g__Decrement_2 : IAction1Ref<float>
        {
            public void Do(ref float a) => Decrement(ref a);
        }

        public struct g__IsInteger_1 : IFunction<int, bool>
        {
            public bool Do(int a) => IsInteger(a);
        }

        public struct g__IsInteger_2 : IFunction<float, bool>
        {
            public bool Do(float a) => IsInteger(a);
        }

        public struct g__IsPositive_1 : IFunction<int, bool>
        {
            public bool Do(int a) => IsPositive(a);
        }

        public struct g__IsPositive_2 : IFunction<float, bool>
        {
            public bool Do(float a) => IsPositive(a);
        }

        #endregion

        // These functions should be able to be static local fuctions. Then
        // all the functionality could be inside the Example method rather
        // than seperated into seperate methods.
        public static int Multiply(int a, int b) => a * b;
        public static float Multiply(float a, float b) => a * b;
        public static void Decrement(ref int a) => a--;
        public static void Decrement(ref float a) => a--;
        public static bool IsInteger(int a) => true;
        public static bool IsInteger(float a) => a % 1 == 0;
        public static bool IsPositive(int a) => a > 0;
        public static bool IsPositive(float a) => a > 0;

        public static T Factorial<T, Decrement, IsInteger, IsPositive, Multiplication>(T a)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where Decrement : struct, IAction1Ref<T>            // -> where Decrement  : [void method(ref T)]
            where IsInteger : struct, IFunction<T, bool>        // -> where IsInteger  : [bool method(T)]
            where IsPositive : struct, IFunction<T, bool>       // -> where IsPositive : [bool method(T)]
            where Multiplication : struct, IFunction<T, T, T>   // -> where Multiply   : [T method(T, T)]
        {
            #region Code that should be unnecessary

            Decrement decrement = default;
            IsInteger isInteger = default;
            IsPositive isPositive = default;
            Multiplication multiplication = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            if (!isInteger.Do(a) || !isPositive.Do(a))           // -> if (!IsInteger(a) || !IsPositive(a))
            {                                                    // -> {
                throw new ArgumentOutOfRangeException(nameof(a), // ->     throw new ArgumentOutOfRangeException(nameof(a),
                    "Parameter is not a positive integer.");     // ->         "Parameter is not a positive integer.");
            }                                                    // -> }
            T current = a;                                       // -> T current = a;
            T result = a;                                        // -> T result = a;
            decrement.Do(ref current);                           // -> decrement(ref current);
            while (isPositive.Do(current))                       // -> while (IsPositive(current))
            {                                                    // -> {
                result = multiplication.Do(result, current);     // ->     result = Multiplication(result, current);
                decrement.Do(ref current);                       // ->     Decrement(ref current);
            }                                                    // -> }
            return result;                                       // -> return result;
        }
    }

    #endregion

    #region Binomial Coefficient

    public static class BinomeialCoefficientExample
    {
        public static void Run()
        {
            /// Instead of passing in g__Decrement_1 you should be able to
            /// pass in Decrement as the generic argument and g__Decrement_1
            /// would be generated at compile time.

            int binomialCoefficientInt_7_5 = // -> int binomialCoefficientInt_7_5 =
                BinomialCoefficient<         // ->     BinomialCoefficient<
                    int,                     // ->         int,
                    g__Factorial_1,          // ->         Factorial<int, Decrement, IsInteger, IsPositive, Multiplication>,
                    g__Division_1,           // ->         Division,
                    g__Multiplication_1,     // ->         Multiplication,
                    g__Subtraction_1         // ->         Subtraction
                    >(7, 5);                 // ->         >(7, 5);

            Console.WriteLine(binomialCoefficientInt_7_5);
        }

        #region Types that should be generated at compile time

        public struct g__Factorial_1 : IFunction<int, int>
        {
            public int Do(int a) => Factorial<int, g__Decrement_1, g__IsInteger_1, g__IsPositive_1, g__Multiplication_1>(a);
        }

        public struct g__Subtraction_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Subtraction(a, b);
        }

        public struct g__Subtraction_2 : IFunction<float, float, float>
        {
            public float Do(float a, float b) => Subtraction(a, b);
        }

        public struct g__Multiplication_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Multiply(a, b);
        }

        public struct g__Multiplication_2 : IFunction<float, float, float>
        {
            public float Do(float a, float b) => Multiply(a, b);
        }

        public struct g__Division_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Division(a, b);
        }

        public struct g__Division_2 : IFunction<float, float, float>
        {
            public float Do(float a, float b) => Division(a, b);
        }

        public struct g__Decrement_1 : IAction1Ref<int>
        {
            public void Do(ref int a) => Decrement(ref a);
        }

        public struct g__Decrement_2 : IAction1Ref<float>
        {
            public void Do(ref float a) => Decrement(ref a);
        }

        public struct g__IsInteger_1 : IFunction<int, bool>
        {
            public bool Do(int a) => IsInteger(a);
        }

        public struct g__IsInteger_2 : IFunction<float, bool>
        {
            public bool Do(float a) => IsInteger(a);
        }

        public struct g__IsPositive_1 : IFunction<int, bool>
        {
            public bool Do(int a) => IsPositive(a);
        }

        public struct g__IsPositive_2 : IFunction<float, bool>
        {
            public bool Do(float a) => IsPositive(a);
        }

        #endregion

        // These functions should be able to be static local fuctions. Then
        // all the functionality could be inside the Example method rather
        // than seperated into seperate methods.
        public static int Subtraction(int a, int b) => a - b;
        public static float Subtraction(float a, float b) => a - b;
        public static int Multiply(int a, int b) => a * b;
        public static float Multiply(float a, float b) => a * b;
        public static int Division(int a, int b) => a / b;
        public static float Division(float a, float b) => a / b;
        public static void Decrement(ref int a) => a--;
        public static void Decrement(ref float a) => a--;
        public static bool IsInteger(int a) => true;
        public static bool IsInteger(float a) => a % 1 == 0;
        public static bool IsPositive(int a) => a > 0;
        public static bool IsPositive(float a) => a > 0;

        public static T Factorial<T, Decrement, IsInteger, IsPositive, Multiply>(T a)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where Decrement : struct, IAction1Ref<T>      // -> where Decrement  : [void method(ref T)]
            where IsInteger : struct, IFunction<T, bool>  // -> where IsInteger  : [bool method(T)]
            where IsPositive : struct, IFunction<T, bool> // -> where IsPositive : [bool method(T)]
            where Multiply : struct, IFunction<T, T, T>   // -> where Multiply   : [T method(T, T)]
        {
            #region Code that should be unnecessary

            Decrement decrement = default;
            IsInteger isInteger = default;
            IsPositive isPositive = default;
            Multiply multiply = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            if (!isInteger.Do(a) || !isPositive.Do(a))           // -> if (!IsInteger(a) || !IsPositive(a))
            {                                                    // -> {
                throw new ArgumentOutOfRangeException(nameof(a), // ->     throw new ArgumentOutOfRangeException(nameof(a),
                    "Parameter is not a positive integer.");     // ->         "Parameter is not a positive integer.");
            }                                                    // -> }
            T current = a;                                       // -> T current = a;
            T result = a;                                        // -> T result = a;
            decrement.Do(ref current);                           // -> decrement(ref current);
            while (isPositive.Do(current))                       // -> while (IsPositive(current))
            {                                                    // -> {
                result = multiply.Do(result, current);           // ->     result = Multiply(result, current);
                decrement.Do(ref current);                       // ->     Decrement(ref current);
            }                                                    // -> }
            return result;                                       // -> return result;
        }

        public static T BinomialCoefficient<T, Factorial, Division, Multiplication, Subtraction>(T n, T k)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where Factorial : struct, IFunction<T, T>         // -> where Factorial      : [T method(T)]
            where Division : struct, IFunction<T, T, T>       // -> where Division       : [T method(T, T)]
            where Multiplication : struct, IFunction<T, T, T> // -> where Multiplication : [T method(T, T)]
            where Subtraction : struct, IFunction<T, T, T>    // -> where Subtraction    : [T method(T, T)]
        {
            #region Code that should be unnecessary

            Factorial factorial = default;
            Division division = default;
            Multiplication multiplication = default;
            Subtraction subtraction = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            return division.Do(                           // -> return Division(
                factorial.Do(n),                          // ->     Factorial(n),
                multiplication.Do(                        // ->     Multiplication(
                    factorial.Do(k),                      // ->         Factorial(k),
                    factorial.Do(subtraction.Do(n, k)))); // ->         Factorial(Subtraction(n, k))));
        }
    }

    #endregion

    #region IEnumerableExample

    public static class IEnumerableExample
    {
        // I don't consider replicating IEnumerable with functions as very pratical,
        // but I included this example simply to show it is possible.

        #region Types that could be generated at compile time

        public struct g__GetEnumerator_1 : IFunction<int[], IntArrayEnumerator>
        {
            public IntArrayEnumerator Do(int[] a) => GetEnumerator(a);
        }

        public struct g__GetCurrent_1 : IFunction<IntArrayEnumerator, int>
        {
            public int Do(IntArrayEnumerator a) => GetCurrent(a);
        }

        public struct g__MoveNext_1 : IFunction1Ref<IntArrayEnumerator, bool>
        {
            public bool Do(ref IntArrayEnumerator a) => MoveNext(ref a);
        }

        public struct g__ConsoleWrite_1 : IAction<int>
        {
            public void Do(int a) => ConsoleWrite(a);
        }

        #endregion

        public static void Run()
        {
            int[] a = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, };

            /// Instead of passing in g__GetEnumerator_1 you should be able to
            /// pass in GetEnumerator as the generic argument and g__GetEnumerator_1
            /// would be generated at compile time.

            Iterate<
                int[],
                int,
                IntArrayEnumerator,
                g__GetEnumerator_1,   // -> GetEnumerator
                g__GetCurrent_1,      // -> GetCurrent
                g__MoveNext_1,        // -> MoveNext
                g__ConsoleWrite_1     // -> ConsoleWrite
                >(a);

            /// The language could also allow for static, compile-time
            /// expressions to be used as generic arguments. Then you could
            /// write something similar to the following. Capture variables
            /// would not be supported as it would need to generate a method
            /// at compile time. This would compile to a seperate static
            /// method (NOT a delegate) that would be wrapped with a compiler
            /// generated struct, and the struct would be used as the generic
            /// parameter.

            //Iterate<
            //    int[],
            //    int,
            //    IntArrayEnumerator,
            //    { a => (0, a) },
            //    { a => a.Item2[a.Item1] },
            //    { (ref a) => (a.Item1 < a.Item2.Length - 1 ? a.Item1 += 1 : 0) != 0 },
            //    { i => Console.WriteLine(i) }
            //    >(a);

            Console.WriteLine();
        }

        // These functions should be able to be static local fuctions. Then
        // all the functionality could be inside the Main method rather
        // than seperated into seperate methods.
        public static IntArrayEnumerator GetEnumerator(int[] a) => (0, a);
        public static int GetCurrent(IntArrayEnumerator a) => a.Item2[a.Item1];
        public static bool MoveNext(ref IntArrayEnumerator a) => (a.Item1 < a.Item2.Length - 1 ? a.Item1 += 1 : 0) != 0;
        public static void ConsoleWrite(int i) => Console.Write(i);

        public static void Iterate<A, B, Enumerator, GetEnumerator, GetCurrent, MoveNext, Action>(A a)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where GetEnumerator : struct, IFunction<A, Enumerator>   // -> where GetEnumerator : [Enumerator (A)]
            where GetCurrent : struct, IFunction<Enumerator, B>      // -> where GetCurrent    : [B (Enumerator)]
            where MoveNext : struct, IFunction1Ref<Enumerator, bool> // -> where MoveNext      : [bool (ref Enumerator)]
            where Action : struct, IAction<B>                        // -> where Action        : [void (B)]
        {
            #region Code that should be unnecessary

            GetEnumerator getEnumerator = default;
            GetCurrent getCurrent = default;
            MoveNext moveNext = default;
            Action action = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            Enumerator enumerator = getEnumerator.Do(a); // -> Enumerator enumerator = GetEnumerator(a);
            do                                           // -> do
            {                                            // -> {
                B current = getCurrent.Do(enumerator);   // ->     B current = GetCurrent(enumerator);
                action.Do(current);                      // ->     Action(current);
            } while (moveNext.Do(ref enumerator));       // -> } while (MoveNext(ref enumerator));
        }
    }

    #endregion

    #region GraphSearchExample

    public static class GraphSearchExample
    {
        public static void Run()
        {
            Console.Write("Path:");
            GraphSearch<
                int,
                int,
                g__Zero_1,
                g__Addition_1,
                g__Compare_1,
                g__Neighors_1,
                g__Heuristic_1,
                g__Cost_1,
                g__Check_1,
                g__ConsoleWrite_1
                >(0, out int totalCost);
            Console.WriteLine();
            Console.Write("Total Cost: " + totalCost);
            Console.WriteLine();
        }

        #region Types that could be generated at compile time

        public struct g__Zero_1 : IFunction<int>
        {
            public int Do() => Zero();
        }

        public struct g__Addition_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Addition(a, b);
        }

        public struct g__Compare_1 : IFunction<int, int, CompareResult>
        {
            public CompareResult Do(int a, int b) => Compare(a, b);
        }

        public struct g__Neighors_1 : IFunction<int, IEnumerable<int>>
        {
            public IEnumerable<int> Do(int a) => Neighbors(a);
        }

        public struct g__Heuristic_1 : IFunction<int, int>
        {
            public int Do(int a) => Heuristic(a);
        }

        public struct g__Cost_1 : IFunction<int, int, int>
        {
            public int Do(int a, int b) => Cost(a, b);
        }

        public struct g__Check_1 : IFunction<int, GraphSearchStatus>
        {
            public GraphSearchStatus Do(int a) => Check(a);
        }

        public struct g__ConsoleWrite_1 : IAction<int>
        {
            public void Do(int i) => ConsoleWrite(i);
        }

        #endregion

        public static int Zero() => 0;

        public static int Addition(int a, int b) => a + b;

        public static CompareResult Compare(int a, int b) =>
            a < b ? CompareResult.Less :
            a > b ? CompareResult.Greater :
            CompareResult.Equal;

        // visualization
        //
        //    [0]-----(1)---->[1]
        //     |               |
        //     |               |
        //    (99)            (2)
        //     |               |
        //     |               |
        //     v               v
        //    [3]<----(5)-----[2]
        //
        //    [nodes in brackets]
        //    (edge costs in parenthases)

        public static IEnumerable<int> Neighbors(int node) => node switch
        {
            0 => new int[] { 1, 3 },
            1 => new int[] { 2 },
            2 => new int[] { 3 },
            _ => throw new Exception(),
        };

        public static int Heuristic(int node) => node switch
        {
            0 => 2,
            1 => 4,
            2 => 2,
            3 => 0,
            _ => throw new Exception(),
        };

        public static int Cost(int a, int b) => (a, b) switch
        {
            (0, 1) => 1,
            (0, 3) => 99,
            (1, 2) => 2,
            (2, 3) => 5,
            _ => throw new Exception(),
        };

        public static GraphSearchStatus Check(int node) =>
            // Note: I did not add a condition to "Break"
            // the algorithm here, but that is possible too.
            node == 3 ? GraphSearchStatus.Goal :
            GraphSearchStatus.Continue;

        public static void ConsoleWrite(int i) => Console.Write(" " + i);

        public static void GraphSearch<Node, Value, Zero, Addition, Compare, Neighbors, Heuristic, Cost, Check, Action>(Node start, out Value totalCost)
            /// There should be syntax sugar on the constraints to
            /// make them look more like function constraints.
            where Zero : struct, IFunction<Value>                          // -> where Zero      : [Value method()]
            where Addition : struct, IFunction<Value, Value, Value>        // -> where Addition  : [Value method(Value, Value)]
            where Compare : struct, IFunction<Value, Value, CompareResult> // -> where Compare   : [CompareResult method(Value, Value)]
            where Neighbors : struct, IFunction<Node, IEnumerable<Node>>   // -> where Neighbors : [IEnumerable<Node> method(Node)]
            where Heuristic : struct, IFunction<Node, Value>               // -> where Heuristic : [Value method(Node))]
            where Cost : struct, IFunction<Node, Node, Value>              // -> where Cost      : [Value method(Node, Node))]
            where Check : struct, IFunction<Node, GraphSearchStatus>       // -> where Check     : [GraphSearchStatus method(Node)]
            where Action : struct, IAction<Node>                           // -> where Action    : [void method(Node)]
        {
            // This graph search implementation uses the A* algorithm.

            #region Code that should be unnecessary

            Zero zero = default;
            Addition addition = default;
            Compare compare = default;
            Neighbors neighbors = default;
            Heuristic heuristic = default;
            Cost cost = default;
            Check check = default;
            Action action = default;

            #endregion

            /// The generic parameter should be invocable directly the ".Do" calls should be unnecessary.

            Node<Node, Value> head = new Node<Node, Value>()
            {
                This = start,
                Previous = null,
                Priority = zero.Do(), // -> Priority = Zero(),
                Cost = zero.Do(), // -> Cost = Zero(),
            };

            void Enqueue(Node<Node, Value> node)
            {
                if (head is null)
                {
                    head = node;
                }
                else if (compare.Do(head.Priority, node.Priority) == CompareResult.Greater)
                {
                    node.Next = head;
                    head = node;
                }
                else
                {
                    Node<Node, Value> value = head;
                    while (value.Next != null ||
                        compare.Do(value.Priority, node.Priority) == CompareResult.Greater)
                    {
                        value = value.Next;
                    }
                    node.Next = value.Next;
                    value.Next = node;
                }
            }

            Node<Node, Value> Dequeue()
            {
                Node<Node, Value> value = head;
                head = head.Next;
                return value;
            }

            static Node<Node, Value> BuildPath(Node<Node, Value> node)
            {
                Node<Node, Value> previous = null;
                while (!(node is null))
                {
                    node.Next = previous;
                    previous = node;
                    node = node.Previous;
                }
                return previous;
            }

            while (!(head is null))
            {
                Node<Node, Value> current = Dequeue();
                GraphSearchStatus status = check.Do(current.This); // -> GraphSearchStatus status = Check(current.This);
                if (status == GraphSearchStatus.Break)
                {
                    break;
                }
                else if (status == GraphSearchStatus.Goal)
                {
                    totalCost = current.Cost;
                    Node<Node, Value> node = BuildPath(current);
                    while (!(node is null))
                    {
                        action.Do(node.This); // -> Action(node.This);
                        node = node.Next;
                    }
                    return;
                }
                else
                {
                    foreach (Node neighbor in neighbors.Do(current.This)) // -> foreach (Node neighbor in Neighbors(current.This))
                    {
                        Value costValue = addition.Do(current.Cost, cost.Do(current.This, neighbor)); // -> Value costValue = Addition(current.Cost, Cost(current.This, neighbor));
                        Enqueue(new Node<Node, Value>()
                        {
                            Previous = current,
                            This = neighbor,
                            Priority = addition.Do(heuristic.Do(neighbor), costValue), // -> Priority = Addition(Heuristic(neighbor), costValue),
                            Cost = costValue,
                        });
                    }
                }
            }
            totalCost = default;
        }

        public enum CompareResult
        {
            Less,
            Equal,
            Greater,
        }

        public enum GraphSearchStatus
        {
            Continue,
            Break,
            Goal,
        }

        public class Node<NodeType, Value>
        {
            public NodeType This;
            public Node<NodeType, Value> Previous;
            public Value Priority;
            public Value Cost;
            public Node<NodeType, Value> Next;
        }
    }

    #endregion
}

Notes

  • There are currently no ways to set default values for generic arguments in C#. When methods have a lot of generic parameters, every single one of those generic parameters would need to be supplied on every usage. There may be ways to add default/optional logic to generic parameters, but I haven't explored that topic yet.

  • This style of code can result in the need to pass in duplicate generic parameters into the same function. For example, the BinomialCoefficient method in the provided examples would require duplicate usage of the Multiplication generic parameter (once for the BinomialCoefficient method and once for the embedded Factorial method). There may be workarounds for this necessity, but I haven't explored that topic yet.