Thursday, December 29, 2011

Calling generic methods

To take advantage of some of the recent API goodness coming from Microsoft, interoperating with generic methods is a must.  Here's an example from System.Linq.Reactive.Observable:

public static IObservable<TResult> Generate<TState, TResult>( 
    TState initialState, Func<TState, bool> condition, 
    Func<TState, TState> iterate, 
    Func<TState, TResult> resultSelector, 
    Func<TState, TimeSpan> timeSelector )

and from the land of Linq, in System.Linq.Enumerable:

public static IEnumerable<TResult> GroupBy<TSource, TKey, TElement, TResult>( 
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    Func<TSource, TElement> elementSelector, 
    Func<TKey, IEnumerable<TElement>, TResult> resultSelector, 
    IEqualityComparer<TKey> comparer )

Much of the goodness of Linq, Reactive Framework and others comes from the ability to chain together generic method calls with minimum specification of type arguments for those calls.   If you are in a statically-typed language such as C#, there are plenty of types floating around to do inferencing on. Of course, with dynamic, C# is not quite the paragon of static typing it once was. The mechanisms C# uses for dynamic call sites surface in the Dynamic Language Runtime and so are available to the wider world. Following the path blazed by IronPython, we have recently enhanced ClojureCLR's ability to interoperate with generic methods.

Start a REPL and start typing:

(import 'System.Linq.Enumerable)
(def r1 (Enumerable/Where [1 2 3 4 5] even?))
(seq r1)                                        ;=> (2 4)

"But of course", you say. Not so fast; under the covers there is merry mischief.

The generic method Where is overloaded with the following signatures.

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, int, bool> predicate)

In the Where call above, the [1 2 3 4 5] is a clojure.lang.PersistentVector. This class implements IEnumerable<Object> and so matches the IEnumerable<TSource> in the first argument position.

The value of even? is a clojure.lang.IFn, more specifically a clojure.lang.AFn. In ClojureCLR, clojure.lang.AFn implements interfaces allowing it take take part in the DLR's generic method type inferencing protocol. One interface answers queries about the arities supported by the function. The function even? reports that it supports one argument and does not support two arguments. Therefore, it supports casting to Func<TSource, bool> but not to Func<TSource, int, bool>, allowing discrimination between the two overloads. (Func<TSource, bool> is a delegate class representing a method taking one argument of type TSource and returning a value of type Boolean.)

From this information, we can pick the overload of Where to use. The value returned by the Where call is of type System.Linq.Enumerable+WhereEnumerableIterator<Object>. This type implements IEnumerable and seq can do the expected thing to it.

If we had a function f that supports one and two arguments, say

(defn f
  ([x] x)  
  ([x y] [x y]))

the type inferencing illustrated in the previous example would not work.

(Enumerable/Where [1 2 3 4 5] f)   ;=> FAIL!
The error messages is quite clear:

ArgumentTypeException Multiple targets could match:  
  Where(IEnumerable`1, Func`2),
  Where(IEnumerable`1, Func`3)

There are two ways around this. The simplest is to use an anonymous function of one argument that calls f:

(Enumerable/Where [1 2 3 4 5] #(f %1))

The second way is to declare the types explicitly. Macros sys-func and sys-action are available to create a function with delegate type matching  System.Func<,...> and System.Action<,...>, respectively.

(Enumerable/Where [1 2 3 4 5] (sys-func [Object Boolean] [x] (f x))))

The first way clearly is preferable. However, when type inferencing does not suffice, sys-func and sys-action can be used. (If delegates of types other then Func<,...> and Action<,...> are required, gen-delegate is available.)

Not just Clojure data structures can participate as sources:

(def r2 (Enumerable/Range 1 10))
(seq r2) ;=> (1 2 3 4 5 6 7 8 9 10)
(seq (Enumerable/Where r2 even?)) ;=> (2 4 6 8 10) 

In fact, any of the following calls will work:

(Enumerable/Where r2 (sys-func [Int32 Boolean] [x] (even? x)))
(Enumerable/Where r2 (sys-func [Object Boolean] [x] (even? x)))
(Enumerable/Where (seq r2) even?)

There are situations where you need to supply type arguments explicitly to a generic method. For example, the following fails:

(def r3 (Enumerable/Repeat 2 5) ;=> FAILS!

The error message states:

InvalidOperationException Late bound operations cannot be performed 
on types or methods for which ContainsGenericParameters is true.

We can cause the type arguments on the Repeat<T> method to be filled in using the type-args macro:

(def r3 (Enumerable/Repeat (type-args Int32) 2 5))
(seq r2) ;=> (2 2 2 2 2)

If you'd like to do Linq-style method concatenation, don't forget the threading macro:

(seq (-> (Enumerable/Range 1 10)
            (Enumerable/Where even?)
            (Enumerable/Select #(* %1 %1))))  ;=> (4 16 36 64 100)

Of course, if you'd like to have the Linq syntax that is available in C#, you are free to write a macro.

No comments:

Post a Comment