Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 438
James Avatar asked Mar 20 '23 00:03

James


1 Answers

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.

like image 52
Gus Avatar answered Apr 01 '23 05:04

Gus