This is the third post in my series of posts on useful Sasa abstractions:
- Sasa.Parsing - type-safe, extensible lexing and parsing framework
- Sasa.Dynamics - type-safe polytypic/reflective programming
This post will deal with a the Sasa.Func static class in the stand-alone core Sasa assembly. This core assembly is concerned mainly with addressing limitations in the core .NET base class libraries. For instance, it contains type-safe, null-safe and thread-safe event operations, extensions on IEnumerable, useful extensions to numbers, and so on.
Sasa.Func is particularly concerned with providing type-safe extensions on delegates. You can view the whole API online. Sasa.Func is available in the core Sasa.dll.
Sasa.Func.Id
The simplest starting point is Sasa.Func.Id. Use this method whenever you need a delegate that simply returns its argument. This is fairly common when using the System.Linq API. Usage:
int[][] nested = new int[][] { { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 }, }; IEnumerable<int> flattened = nested.SelectMany(Func.Id); foreach (var x in flattened) Console.Write(" {0},"x); // prints out: // 0, 1, 2, 3, 4, 5, 6, 7, 8,
Sasa.Func.Create
Func.Create is a type-safe wrapper for Delegate.CreateDelegate. Usage:
class Foo { public int Bar(int i); } ... var delegate1 = Func.Create<Func<int, int>>( new Foo(), typeof(Foo).GetMethod("Bar"));
However, Func.Create is strictly more powerful than Delegate.CreateDelegate because it addresses certain limitations of the CLR I discovered two years ago. It was previously impossible to create an open instance delegate to either virtual methods, or to generic interface methods. Func.Create handles both cases by automatically generating a small dispatch thunk which wraps the invocation for you, and returns a delegate of the appropriate type.
The example above uses reflection to access the method metadata, but Sasa does provide a type-safe way to obtain the MethodInfo without reflection via Sasa.Types. This will be covered in a future post.
Sasa.Func.Combine
Func.Combine is basically a type-safe wrapper for Delegate.Combine. Usage:
Action<int> delegate1 = i => Console.WriteLine("i = {0}", i); Action<int> delegate2 = i => Console.WriteLine("i*2 = {0}", i * 2); Action<int> combined = Func.Combine(delegate1, delegate2); // invoking combined(3) prints out: // i = 3 // i*2 = 6
Sasa.Func.Remove
Similar to Func.Combine, Func.Remove is a type-safe wrapper for Delegate.Remove. Usage:
Action<int> delegate1 = i => Console.WriteLine("i = {0}", i); Action<int> delegate2 = i => Console.WriteLine("i*2 = {0}", i * 2); Action<int> combined = Func.Remove(Func.Combine(delegate1, delegate2), delegate2); // invoking combined(3) prints out: // i = 3
Sasa.Func.AsFunc
One somewhat frustrating limitation of the CLR is that System.Void is not considered a value, and so cannot be used as a type parameter that is used in return position. So for instance, you can't create a Func<void>. This relegates void to second-class status, where all other types produce values as first-class citizens.
This effectively divides the logical space of function types into those that return void (System.Action), and those that return a value (System.Func), and you cannot mix the two. Every operation that abstracts over the return type must then be written twice: once for functions that return a value, and again for functions that return void.
Sasa.Func.AsFunc provides a wrapper around the various System.Action delegates, effectively transforming them into the corresponding System.Func instance with a return value of Sasa.Empty. Func.AsFunc is also an extension method on the System.Action overloads, to make this wrapping as concise as possible. Usage:
Action<int> delegate1 = i => Console.WriteLine("i = {0}", i); Func<int, Empty> delegate2 = delegate1.AsFunc(); // invoking delegate2(3) prints out: // i = 3
Sasa.Func.Getter
A somewhat recurring pattern in C# programming is generating a delegate to access the value of a property. It's a little wasteful to generate a whole new delegate that closes over an object instance and then accesses the property, considering the object already has a method getter, ie. for property Foo, the C# compiler generates a get_Foo method.
Sasa.Func.Getter allows you specify an expression naming a property, and will return a typed delegate to the direct method getter for that property. Usage:
struct Foo { public int SomeInt { get; set; } } ... var getter = Func.Getter<Foo, int>(x => x.SomeInt); var foosInt = getter(someFoo);
At the moment, the whole expression tree is generated every time this method is invoked, but a future extension to Sasa's ilrewriter will eliminate this entirely and generate direct operations on CIL metadata.
Sasa.Func.Setter
The dual to Sasa.Func.Getter, Sasa.Func.Setter obtains a typed delegate for the direct setter method of an object. Usage:
struct Foo { public int SomeInt { get; set; } } ... var setter = Func.Setter<Foo, int>(x => x.SomeInt); setter(someFoo, 99);
Similar to Sasa.Func.Getter, this will soon be inlined completely when using Sasa's ilrewriter.
Sasa.Func.Invoke
Sasa.Func.Invoke is a simple typed raw method invocation, ie. a typed equivalent of Delegate.DynamicInvoke. It's defined as an extension method on MethodInfo for clarity. Usage:
// SomeMethod is of type (string,float) -> int MethodInfo someMethod = typeof(Foo).GetMethod("SomeMethod"); var arg1 = 3.4F; var returnInt = someMethod.Invoke<int>("arg0", arg1);
Sasa.Func.Constant
Use Sasa.Func.Constant whenever you need a delegate that always returns a constant value, regardless of the parameter passed in. Usage:
int[] numbers = new int[] { 0, 1, 2, 3 }; var sameNumber = Func.Constant<int, int>(99); foreach (var x in numbers.Select(sameNumber)) { Console.Write(" {0},", x); } // prints out // 99, 99, 99, 99,
Sasa.Func.Open, Sasa.Func.OpenAction
A delegate designating Object.ToString can take two forms:
- The typical closed delegate has type System.Func<string> and encapsulates the reference to the object being converted to a string.
- The open instance delegate would have type System.Func<object, string>, so the object being converted to a string must be passed in each time.
Sasa.Func.Open and Sasa.Func.OpenAction methods serve the same purpose, namely to create a so-called open instance delegate, where the 'this' parameter is not encapsulated within the delegate itself, but is itself the first parameter passed to the delegate.
This allows you to reuse the same delegate multiple times on different objects without needing a different delegate for each object you want to convert to a string, or whatever other operation desired. This is also how efficient dispatch works in Sasa.Dynamics, ie. the cached delegate in Type<T>.Reduce(IReduce, T) is a statically computed, cached open instance delegate to the method that handles type T in the IReduce interface.
Usage:
class Foo { public int Value { get; set; } public int Bar() { return Value; } } ... var foo = new Foo { Value = 3 }; Func<int> closed = foo.Bar; Func<Foo, int> open = Func.Open<Foo, int>(closed); var foo2 = new Foo { Value = 9999 }; Console.WriteLine(open(foo)); Console.WriteLine(open(foo2)); // prints: // 3 // 9999
Sasa.Func.VOpen, Sasa.Func.VOpenAction
These methods serve the same purpose as Sasa.Func.Open and Sasa.Func.OpenAction above, but they operate directly on value types (hence the V prefix). As before, the first parameter to a method is always a reference to the object being manipulated, but structs aren't reference types, so a method for struct T actually accepts a "ref T" as its first argument. Thus, open instance delegates that modify their struct argument must have a different signature, namely that of Sasa.VAction [1, 2, 3, 4] and Sasa.VFunc [1, 2, 3, 4] all of which take a "ref T" as the first argument.
Sasa.Func.VOpen creates open instance delegates that return values, ie. Sasa.VFunc, and Sasa.Func.VOpenAction creates open instance delegates that return void, ie. Sasa.VAction. Usage:
struct Foo { public int Value { get; set; } public void Double() { Value *= 2; } } ... var foo = new Foo { Value = 3 }; Action closed = foo.Double; VAction<Foo> open = Func.VOpen<Foo>(closed); var foo2 = new Foo { Value = 444 }; open(ref foo); open(ref foo2); Console.WriteLine(foo.Value); Console.WriteLine(foo2.Value); // prints: // 6 // 888
I'm not entirely satisfied with the naming of Sasa.Open, Sasa.VOpen, Sasa.OpenAction, and Sasa.VOpenAction, so I'm very open to suggestions. Part of the problem is that overload resolution does not take type constraints into account, so even though Open and VOpen have constraints T:class and T:struct respectively, they need a different name or the compiler complains of ambiguous methods. We also seem to need different names for Open/OpenAction or there is another ambiguity as to whether we want a delegate that returns a value, or that returns void.
Sasa.Func.Operator
Sasa.Func.Operator creates a delegate for the operator provided:
var add = Func.Operator<Func<int, int, int>>(Operator.Add); Console.WriteLine(add(1, 2)); // output: // 3
If the types involved are all primitives and so have no operators, then a function is dynamically generated with the corresponding CIL instructions for the operator. Sasa.Func.Operator is the main function powering Sasa.Operator<T>, Sasa.Operators<T0, T1>, and Sasa.Operators<T0, T1, T2>.
Sasa.Func.Fix
Sasa.Func.Fix are a set of overloaded methods to generate recursive lambdas. Delegates built from lambdas can't refer to themselves to make recursive calls. Sasa.Func.Fix addresses this by providing what's known as a fixpoint function. Usage:
// equivalent recursive function: // int Sum(int x) { return x > 0 ? x + Sum(x-1) : 0; } var f1 = Func.Fix<int, int>( f => x => x > 0 ? x + f(x - 1) : 0); Console.WriteLine(f1(2)); Console.WriteLine(f1(5)); // prints // 3 // 15
Sasa.Func.Coerce
The type language for delegates is somewhat limited compared to the type language for interfaces and classes. For instance, interface methods support first-class polymorphism, where delegates do not. Combined with the fact that delegates are nominal types in their own right, this causes a proliferation of delegate types that are identical in type signature, but differ in nominal type and so cannot be substituted for each other. For instance, System.Predicate<T> is equivalent to System.Func<T, bool>, but you cannot use a delegate of one type in a place where the other delegate type is expected.
The Sasa.Func.Coerce extension method allows you to coerce one delegate type into another equivalent type. Usage:
Predicate<int> isthreep = x => x == 3; Func<int, bool> isthreef = isthreep.Coerce<Func<int, bool>>(); Console.WriteLine("{0}, {1}", isthreep(3), isthreef(3)); Console.WriteLine("{0}, {1}", isthreep(4), isthreef(4)); // prints: // true, true // false, false
Sasa.Func.Generate
The Sasa.Func.Generate method overloads are the real workhorses behind the scenes. These methods eliminate all the boilerplate in generating and debugging a DynamicMethod, and is statically typed in the delegate type to generate. The most useful overloads by far are the ones that accept a boolean "saveAssembly" parameter. If saveAssembly:true is passed in, then the method is generated in a newly created assembly that is written to disk. You can then run peverify on it to check for any verification errors in your dynamically generated code. A single change to this variable can switch between debugging and production modes.
Func.Generate is used throughout Sasa, even in Sasa.Func to create the thunks to dispatch open instance delegates for virtual methods and generic interface methods. Usage:
Func<int, int> addone = Func.Generate<Func<int, int>>( type: typeof(int), methodName: typeof(int).Name + "_addone", generator: il => { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Add); il.Emit(OpCodes.Ret); }); Console.WriteLine("{0}, {1}, {2}", addone(0), addone(9), addone(998)); // prints // 1, 10, 999
Comments