开发者

F# pattern matching on types of tuples

开发者 https://www.devze.com 2022-12-19 04:07 出处:网络
I have a curried function that I\'d like it to support different types of parameters, that are not on a inheritance relationship:

I have a curried function that I'd like it to support different types of parameters, that are not on a inheritance relationship:

type MyType1 = A | B of float
type MyType2 = C | D of int

What I tried to do is:

let func x y =
    match (x, y) with
    | :? Tuple<MyType1, MyType1> -> "1, 1"
    | _ -> "..."

However this is not possible. F# complains:

The type ''a * 'b' does not have any proper subtypes and cannot be used as the source of a type test or runtime coercion.

What is an elegant way to do this?

EDIT: Let me try to clarify this.

I have two similar, but distinct, types. I can very easily convert one type to another. I want to define a binary operation that will act on entities of those types, but I'd like to expose a single operation to the client.

That is, instead of providing:

let op11 (x : MyType1) (y : MyType1) = // do something useful
let op12 (x : MyType1) (y : MyType2) =
    // convert y to MyType1
    let y' = // ...
    // dispatch to op11
    op11 x y'
let op21 (x : MyType2) (y : MyType1) = // similar
let op22 (x : MyType2) (y : MyType2) = // similar

what I would like is to expose a single function to client code:

let op (x : obj) (y : obj) = // ...

This is like simulating the behav开发者_如何转开发ior of method overloading, but with curried functions.


Your code doesn't work, because F# generalizes the type of arguments to a type parameter. I think you can't dynamically test whether a type 'a * 'b can be converted to type MyType1 * MyType2 (though this is a bit confusing to me). In any case, you can write a function that takes two arguments of type obj and tests them separately using two :? patterns:

type MyType1 = A | B of float 
type MyType2 = C | D of int

let func (x:obj) (y:obj) = 
    match (x, y) with 
    | (:? MyType1 as x1), (:? MyType1 as x2) -> 
        printfn "%A %A" x1 x2
    | _ -> 
        printfn "something else" 

func A (B 3.0) // A B 3.0
func A (D 42)  // something else

Anyway, it would be interesting to know why do you want to do this? There may be a better solution...

EDIT (2) So, from all 4 two-element combinations of T1 and T2, you want the function that can take 3. Is that correct (T1 * T1, T1 * T2 and T2 * T2)? In that case, you can't write a fully safe curried function, because the type of second argument would "depend" on the type of first argument (if the first argument has a type T2, then the second argument also has to be T2 (otherwise it can be T1 too)).

You can write a safe non-curried function that takes an argument of the following type:

type MyArg = Comb1 of T1 * T1 | Comb2 of T1 * T2 | Comb3 of T2 * T2

The type of the function would be MyArg -> string. If you want a curried function, you can define a type which allows you to use either T1 or T2 as both first and second argument.

type MyArg = First of T1 | Second of T2

Then, your curried function will be MyArg -> MyArg -> string. But note that if one combination of argument types is not allowed (if I understand you correctly, T2 * T1 shouldn't be allowed). In this case, your function will simply have to throw an exception or something like that.


There are essentially three different ways to accomplish this.

The first is to sacrifice static typing by upcasting to obj as you suggested:

let func x y =
  match box x, box y with
  | (:? MyType1 as x), (:? MyType1 as y) ->
      ...

This is virtually always an awful solution because it results in the introduction of unnecessary run-time type checks:

  | _ -> invalidArg "x" "Run-time type error"

The only place I have seen this work well is library code designed specifically for users to invoke from F# interactive sessions where compile-time and run-time type errors effectively occur at the same time and dynamic typing can be more concise. For example, our F# for Visualization library allows the user to try to visualize any value of any type using this technique to invoke custom visualization routines for different (known) types (read more).

The second is to replace your two separate types with a single type:

type MyType1 = A | B of float | C | D of int   

The third solution is to introduce a new type that unifies just those two types:

type MyType = MyType1 of MyType1 | MyType2 of MyType2


I have two similar, but distinct, types. I can very easily convert one type to another. I want to define a binary operation that will act on entities of those types, but I'd like to expose a single operation to the client.

This function should decide which of the opXY to call, properly downcasting types.

This is one of those cases where the correct answer isn't "here's how you do it", but instead "don't do it like that". There's no real advantage using F# if you aren't going to stick with its idioms and type-checker.

From my point of view, if your types are so similar, they should be merged into the same type:

type MyType = A | B of float | C | D of int

Barring that, you can wrap your two types in another type:

type composite =
    | MyType1Tuple of MyType1 * MyType1
    | MyType2Tuple of MyType2 * MyType2

Another layer of indirection never hurt anyone. But at least now your clients can wrap the objects with another without losing type safety.

And barring all of that, expose two separate methods for your different types.


This smells very fishy, you should describe the bigger problem context as it seems like you shouldn't be in this situation. That said:

type MyType1 = A | B of float 
type MyType2 = C | D of int 

// imagine this adds floats, and C -> A (=0.0) and D -> B
let DoIt x y =
    match x, y with
    | A, A -> 0.0
    | A, B z -> z
    | B z, A -> z
    | B z1, B z2 -> z1 + z2

let Convert x =
    match x with
    | C -> A
    | D i -> B (float i)

let Func (x:obj) (y:obj) =
    match x, y with
    | (:? MyType2 as xx), (:? MyType2 as yy) -> DoIt (Convert xx) (Convert yy)
    | (:? MyType1 as xx), (:? MyType2 as yy) -> DoIt xx (Convert yy)    
    | (:? MyType2 as xx), (:? MyType1 as yy) -> DoIt (Convert xx) yy
    | (:? MyType1 as xx), (:? MyType1 as yy) -> DoIt xx yy    
    | _ -> failwith "bad args"
0

精彩评论

暂无评论...
验证码 换一张
取 消