Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern matching on generics in a non-generic method implementing an interface

I have a frustrating problem. I'm building a view engine in ASP.NET MVC and are implementing the interface IViewEngine. In one of the methods I'm trying to dynamically figure out the type of the result of a view. Sometimes the result is a template (with the type Template<'key>). The keys are used to target a placeholder in the template, and the idea is to use a discriminated union, potentially unique for each web site. It could look like this:

type MasterKey = | HeadContent | HeaderContent | MainContent | FooterContent
let MasterTemplate : Template<MasterKeys> = ...

Now, the problem is this: since I'm implementing an interface, I have no control over the method signature. Since I can not add a generic type parameter, the 'a will be converted to an obj and the Template will not be a match below:

   match result with
   | :? foo -> ...
   | :? bar -> ...
   | :? Template<'a> -> ...

Any ideas?

like image 921
Johan Jonasson Avatar asked Jun 30 '26 10:06

Johan Jonasson


1 Answers

Unfortunately, there is no way to do this nicely. If you have control over the Template<'T> type, then the best option is to create a non-generic interface (e.g. ITemplate) and implement that in the Template<'T> type. Then you can just check for the interface:

| :? ITemplate as t -> ...

If that's not the case, then your only option is to use some reflection magic. You can implement an active pattern that matches when the type is some Template<'T> value and returns a list of types (System.Type objects) that were used as generic arguments. In your pseudo-code, you wanted to get this as the generic type parameter 'a - it isn't possible to get that as compile-time type parameter, but you can get that as runtime type information:

let (|GenericTemplate|_|) l =
  let lty = typeof<list<int>>.GetGenericTypeDefinition()
  let aty = l.GetType()
  if aty.IsGenericType && aty.GetGenericTypeDefinition() = lty then
    Some(aty.GetGenericArguments())
  else 
    None

Now you can write the following pattern matching code:

match result with
| GenericTemplate tys ->
    // ...

The last problem is - how can you use this runtime type information to run some generic code. The best option I can think of is to invoke a generic method (or a function) using reflection - then you can specify the runtime type information as a generic parameter and so the code can be generic. The simplest option is to invoke static member of a type:

 type TemplateHandler =
   static member Handle<'T>(arg:Template<'T>) =
     // This is a standard generic method that will be 
     // called from the pattern matching - you can write generic
     // body of the case here...
     "aaa"

| :? GenericTemplate tys ->
    // Invoke generic method dynamically using reflection
    let gmet = typeof<TemplateHandler>.GetMethod("Handle").MakeGenericMethod(tys)
    gmet.Invoke(null, [| result |]) :?> string // Cast the result to some type

The key idea is that you move body of a pattern matching (which cannot have generic type parameters) into a method (that can have generic type parameters) and run the method dynamically using reflection.

You could change the code to use let function instead of static member as well - it is only slightly more difficult to find the function using reflection.

like image 155
Tomas Petricek Avatar answered Jul 03 '26 06:07

Tomas Petricek



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!