Reputation: 453
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
Upvotes: 4
Views: 2047
Reputation: 26204
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.
Upvotes: 5
Reputation: 29120
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
Upvotes: 2