Skip to content

Calling generic methods

Robert Johnson edited this page Oct 2, 2022 · 4 revisions

We're talking here about generic methods, not methods in generic classes.

The use of generic methods in .Net libraries has increased significantly in recent years. Linq, for example, relies on generic methods as extension methods to other types. Linq makes significant use of dynamic call sites and type inferencing on generic method type parameters for its magic.

ClojureCLR shares some of that magic.

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

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
)

When analyzing (Enumerable/Where [1 2 3 4 5] even?), the [1 2 3 4 5] is a clojure.lang.PersistentVector, which supports IEnumerable<Object>and hence can match against the IEnumerable<TSource> in the first argument position. The value of even? is a clojure.lang.IFn, specifically a clojure.lang.AFn. In ClojureCLR, clojure.lang.AFn implements interfaces related to reporting the number of arguments supported in calls and to participating in generic type parameter inferencing. The method 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 for a method taking one argument of type TSource and returning a value of type 'bool'.)

If we had a function f that supports one and two arguments, the type inferencing illustrated in the previous example would not work.

user=> (Enumerable/Where [1 2 3 4 5] f)
ArgumentTypeException Multiple targets could match: 
   Where(IEnumerable`1, Func`2), 
   Where(IEnumerable`1, Func`3)  .CallSite.Target (:0)

There are two ways around this. One way is to write a simple anonymous function taking one argument that calls f:

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

The second way is to explicitly declare types. Macros sys-func and sys-action are available to create from an IFn delegates of type System.Func<,...> and System.Action<,...>:

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

The first is preferable. However, when type inferencing does not suffice sys-func and sys-action can allow you to do the inference for the system.

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

This used to fail due to problems with Range iterator conversions, but it works now. However, I'll still use it as an example of how to use sys-func:

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

BTW, the following might work if r2 did not support IEnumerable in some flavor but did support conversion to an ISeq:

(Enumerable/Where (seq r2) (even? x))

This works because the type implementing the sequence on r2 does support IEnumerable<Object>.

There are some cases where you need to explicitly supply type arguments 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. System.Reflection.RuntimeM ethodInfo.ThrowNoInvokeException (:0)

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))
                ; use type-args to supply the type parameters to the generic method
(seq r3)        ;=> (2 2 2 2 2)
Clone this wiki locally