4

I'm writing a adapter class to map IEnumerable<'T> to IDataReader the full source is at https://gist.github.com/jsnape/56f1fb4876974de94238 for reference but I wanted to ask about the best way to write part of it. Namely two functions:

member this.GetValue(ordinal) =
    let value = match enumerator with
        | Some e -> getters.[ordinal](e.Current)
        | None -> raise (new ObjectDisposedException("EnumerableReader"))

    match value with
        | :? Option<string> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj
        | :? Option<int> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj
        | :? Option<decimal> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj
        | :? Option<obj> as x -> if x.IsNone then DBNull.Value :> obj else x.Value
        | _ -> value

This function must return an object but since the values are being passed can be any F# option type which isn't understood by downstream functions such as SqlBulkCopy I need to unpack the option and convert it to a null/DBNull.

The above code works but I feel its a bit clunky since I have to add new specialisations for different types (float etc). I did try using a wildcard | :? Option <_> as x -> in the match but the compiler gave me a 'less generic warning and the code would only match Option< obj >.

How can this be written more idiomatically? I suspect that active patterns might play a part but I've never used them.

Similarly for this other function:

member this.IsDBNull(ordinal) =
    match (this :> IDataReader).GetValue(ordinal) with
        | null -> true
        | :? DBNull -> true
        | :? Option<string> as x -> x.IsNone
        | :? Option<int> as x -> x.IsNone
        | :? Option<decimal> as x -> x.IsNone
        | :? Option<obj> as x -> x.IsNone
        | _ -> false

I don't care what kind of Option type it is I just want to check against IsNone

dbc
  • 104,963
  • 20
  • 228
  • 340
James
  • 433
  • 4
  • 14
  • 2
    I just want to add that in your `IsDBNull` member you do not have to check for `IsNone`, since `None` is represented as `null` internally and will be matched by the first case. `None :> obj |> function null -> printfn "null" | _ -> printfn "something else"` will print "null". – Nikon the Third Jul 19 '14 at 18:39

2 Answers2

5

I think you should use some reflection techniques like this:

open System

let f (x:obj) =
    let tOption = typeof<option<obj>>.GetGenericTypeDefinition()
    match x with
    | null -> printfn "null"; true
    | :? DBNull -> printfn "dbnull"; true
    | _ when x.GetType().IsGenericType && x.GetType().GetGenericTypeDefinition() = tOption ->
        match x.GetType().GenericTypeArguments with
        | [|t|] when t = typeof<int> -> printfn "option int"; true
        | [|t|] when t = typeof<obj> -> printfn "option obj"; true
        | _                          -> printfn "option 't" ; true

    | _ -> printfn "default"; false


let x = 4 :> obj
let x' = f x  //default

let y = Some 4 :> obj
let y' = f y  // option int

let z = Some 0.3 :> obj
let z' = f z  // option 't

UPDATE

In fact if you are just interested to check the IsNone case of all option types and don't want to use reflection you don't need the other cases, they will fall in the null case since None is compiled to null. For example with the previous function try this:

let y1 = (None: int option)  :> obj
let y1' = f y1  // null

let z1 = (None: float option)  :> obj
let z1' = f z1  // null

It's being handled with the first case (the null case)

For the GetValue member, I had a look at your gist and since you defined the generic 'T already in the type that contains that member you can just write:

match value with
| :? Option<'T> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj

for all option types.

Gus
  • 25,839
  • 2
  • 51
  • 76
  • I've tried to stay away from reflection for perf reasons. This is going to be used for bulk loading tens of millions of rows of data into Sql Server so this code could be called many times. In the full class I compile code specific for each property getter so as not to incur reflection overhead. – James Jul 19 '14 at 20:57
  • Yes, that was mentioned in the comments to my question. I'm still interested in the answer to implementing GetValue too. – James Jul 19 '14 at 22:22
  • @James true, it was mentioned in the comments. I just added in the update a proposal for the GetValue. – Gus Jul 19 '14 at 22:36
  • Unfortunately, the 'T from the class is not the option type so I don't think this works. – James Jul 19 '14 at 23:44
  • Note that you can save a few keystrokes by using `let tOption = typedefof – kvb Jul 20 '14 at 02:24
  • So no one has come forward with a non-reflection answer so I'm marking this as an answer for now but to be honest there is as much code here as adding each type individually - its an IDataReader so the common case only has to deal with primitive types int, string, datetime etc - 11 lines of code and no reflection. – James Jul 22 '14 at 20:12
2

As Gustavo's answer suggests, you should use reflection for this. There's no other way to cast from an obj to an option<'a> type if the argument 'a is unknown at compile time. Instead you have to inspect the argument as a System.Type object and then decide what to do next.

A general for doing this is to setup a function that can take any option type as an argument, and return something whose type isn't dependent on the argument to the option type. This function can then be called via reflection after establishing what the argument type is.

To define the function that can take any option type as an argument, a helper interface is useful, because we can define a generic method inside that interface:

type IGenericOptionHandler<'result> =
    abstract Handle<'a> : 'a option -> 'result

Notice that the interface as a whole is generic over the return type 'result of the Handle method, but the internal 'a parameter is only mentioned in the definition of the method itself.

We can now define a function for calling this interface:

let handleGeneric
        (handle : IGenericOptionHandler<'result>)
        (x : obj)  // something that might be an option type
        (defaultValue : 'result) // used if x is not an option type
      : 'result =

    let t = x.GetType()
    if t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<_ option>
        then
            match t.GetGenericArguments() with
            | [|tArg|] ->
                handle
                  .GetType()
                  .GetMethod("Handle")
                  .MakeGenericMethod([|tArg|])
                  .Invoke(handle, [|x|])
                 :?> 'result
            | args -> failwith "Unexpected type arguments to option: %A" args
        else defaultValue

And finally we can call it conveniently with an object expression, e.g. the following will act as a generic option type detector similar to IsDBNull above - you'd need to add the special case for DBNull in the defaultValue parameter to replicate it exactly.

Option.handleGeneric
    { new IGenericOptionHandler<bool> with member this.Handle _ = true }
    (Some 5)
    false
Ganesh Sittampalam
  • 28,821
  • 4
  • 79
  • 98