James
James

Reputation: 453

How to deal with option values generically in F#

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

Answers (2)

Gus
Gus

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

Ganesh Sittampalam
Ganesh Sittampalam

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

Related Questions