Search code examples
f#option-typeidatareader

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


Solution

  • 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.