简体   繁体   中英

Is there a way in F# to write a generic function that will accept either a Tuple or Single value?

I am writing some code to process a large data file. The CSV TypeProvider will return NaN for a given value if the field is none. I have quite a few fields, so it's cleaner if I write a helper function to check for that NaN and return a None instead. Not knowing how to write a more generic helper function, I came up with this:

let (!?) x = if Double.IsNaN(x) then None else Some (decimal x)
let (!?) (x, y) = 
    match (x, y) with
    | (x, y) when not(Double.IsNaN(x)) && not (Double.IsNaN(y))  -> Some (decimal x, decimal y)
    | (_, _) -> None

Unfortunately, my attempt to operator overload is not working correctly and duplicating code isn't great either. A newbie asks, is there a better way to do this?

(I am aware of something like PreferOptionals, but I need to do this more selectively)

You need to make them static members of an intermediate type:

open System

type T = T with
    static member ($) (T, x) = if Double.IsNaN(x) then None else Some (decimal x)
    static member ($) (T,(x, y)) = 
        match (x, y) with
        | (x, y) when not(Double.IsNaN(x)) && not (Double.IsNaN(y))  -> Some (decimal x, decimal y)
        | (_, _) -> None

let inline parse x = T $ x


// Usage

let a = parse (1. , 2.)
// val a : (decimal * decimal) option = Some (1M, 2M)

let b = parse 1.
// val b : decimal option = Some 1M

Also note that it's better to use a binary operator to send also the intermediate type. This way you delay overload resolution.

You can also use named functions but it's more verbose.

EDIT Regarding delaying overload resolution.

First of all, I said in order to write it as a named function you have to write the constraints by hand:

let inline parse (x: ^a) =
    ((^b or ^a) : (static member ($) : ^b -> ^a -> _) T,x)

Note that the other answer that was later added is exactly the same as this, the only difference is that it uses a name ToOption for the static member instead of an operator.

Ok, now let's try to get rid of T , we can do it to some extent:

type T = T with
    static member ($) x = if Double.IsNaN(x) then None else Some (decimal x)
    static member ($) ((x, y)) = 
        match (x, y) with
        | (x, y) when not(Double.IsNaN(x)) && not (Double.IsNaN(y))  -> Some (decimal x, decimal y)
        | (_, _) -> None

let inline parse (x: ^a) =
    let call (_:'b) = ((^b or ^a) : (static member ($) : ^a -> _) x)
    call T

Note that I had to create a way to unify ^b with T , that's why I added a call function.

Now this still works and it's a valid solution which cleans up the overload signature and add some more noise in the parse function, which in many scenarios is a good trade-off. But what if I remove completely the ^a parameter from the trait call?

let inline parse (x: ^a) =
    let call (_:'b) = (^b : (static member ($) : ^a -> _) x)
    call T

this fails with

~vs6086.fsx(11,27): error FS0043: A unique overload for method 'op_Dollar' could not be determined based on type information prior to this program point. A type annotation may be needed.

Known return type: 'b

Known type parameter: <  ^a >

Candidates:
 - static member T.( $ ) : (float * float) -> (decimal * decimal) option
 - static member T.( $ ) : x:float -> decimal option

Why is that?

Because now the F# compiler knows at the trait-call site all the type variables involved.

Before this last change ^a was unknown and ^b was unified with T , but since the other one was unknown, overload resolution was delayed to each future individual call of the parse function, which is doable since it's declared inline.

By knowing all type parameters, it tries to apply standard .Net overloading resolution and fails because there are 2 candidates.

Hope this explanation makes sense.

An F# binding has a single definition. Defining it again simply shadows the previous definition.

let f x = x
let f x = x * x //previous f is now shadowed

Here's a way to use statically resolved types to achieve the same result. We're defining the members on an intermediate type (a single-case DU) called Converter :

type Converter = Converter with
    static member ToOption (_ : Converter, value) = 
        if Double.IsNaN(value) then None else Some(value)

    static member ToOption (_ : Converter, (x, y)) = 
        if Double.IsNaN(x) || Double.IsNaN(y) then None else Some(x, y)

let inline (!?) (x : ^a) =
    ((^b or ^a) : (static member ToOption : ^b * ^a -> _) (Converter, x))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM