Tomas Aschan
Tomas Aschan

Reputation: 60564

How do I runtime-instantiate a generic type in F#?

I wish I could do the following:

type Logger = {
    warn : System.Type -> string -> unit
    // also methods for error, info etc
}

// during application startup
let warn (serviceProvider : IServiceProvider) (t : Type) s =
    let loggerType = typeof<ILogger<'a>>.MakeGenericType(t)
    let logger = serviceProvider.GetService(loggerType) :?> ILogger
    logger.LogWarning(s)

let log = { warn = warn; ... } // with similar setups for info, error etc

Then, I'd be able to partially apply with log, and deep inside the app I could

log.warn typeof<Foo> "a warning message"

which would log a message using the type Foo to determine the log category.

The troublesome expression is

typeof<ILogger<'a>>.MakeGenericType(t)

where F# seems to infer the type of 'a in the generic definition to be obj, and so the call to MakeGenericType fails with an exception saying that the type definition I'm calling it on is not generic.

The equivalent C# would be

typeof(ILogger<>).MakeGenericType(t)

but typeof<ILogger<>> doesn't compile in F#.

How do I instantiate a generic type like this, where the type argument is only known at runtime, in F#?

Upvotes: 1

Views: 719

Answers (2)

Fyodor Soikin
Fyodor Soikin

Reputation: 80714

The syntax you're looking for is typedefof<ILogger<_>> - that will give you the non-instantiated type ILooger<_>, on which you can then call MakeGenericType.

However, I would rather recommend you rethink your architecture. Why instantiate types at runtime? That's slower and less safe. In this case, I see no reason to do it.

Better pass your types as generic parameters. Of course you can't have record members be generic functions, so that's a bummer. However, F# does offer another facility for this sort of thing - interfaces. Interface methods can be fully generic:

type Logger =
    abstract member warn<'logger> : string -> unit

let mkLog (serviceProvider : IServiceProvider) =
    { new Logger with
        member warn<'logger> s =
            let logger = serviceProvider.GetService(typeof<ILogger<'logger>>) :?> ILogger
            logger.LogWarning(s)
    }

let log = mkLog serviceProvider

// usage:
log.warn<Foo> "a warning message"

Granted, the syntax for creating an instance of such type is a bit clunky, but you only have to do it once.

Upvotes: 5

Tomas Petricek
Tomas Petricek

Reputation: 243041

The F# equivalent of typeof(ILogger<>) in C# is to use the typedefof function:

typedefof<ILogger<_>>.MakeGenericType(t)

This is still a normal function that takes a fully instantiated type - the _ placeholder will be automatically filled with obj, but the typedefof function does not return the type, but its generic type definition. You could also do the same by calling GetGenericTypeDefinition on the result of typeof, but typedefof is a nicer shortcut!

Upvotes: 3

Related Questions