F# Testing for Base Types With Pattern Matching and Boxing of Tuples

72 Views Asked by At

I try to understand the concept of boxing and testing for base types, especially with tuples.

I have two objects from an external C# library that have different concrete types but share a common base type:

let o1 = ConcreteType1() // has base type BaseType
let o2 = ConcreteType2() // also has base type BaseType

If both o1 and o2 are derived from BaseType, I have to perform some special comparison logic, so I'd like to test whether the elements of a tuple (o1, o2) both have base type BaseType.

Based on the answers to this question, I gather that I have to box each element of the type separately and perform the type tests on the individual elements, so that base types are considered:

match box o1, box o2 with
| (:? BaseType), (:? BaseType) -> // special logic with o1, o2
| _ -> // nope, some other behavior

My understanding is that simply boxing the tuple itself will not upcast the individual elements to obj, and the test for their base types will therefore not work:

match box (o1, o2) with
| :? (BaseType * BaseType) -> // never hit, because elements are not upcast to obj
| _ -> // ...

Is this the correct explanation for the observed behavior, or are there other mechanisms involved?

1

There are 1 best solutions below

0
On BEST ANSWER

It's true that boxing a tuple doesn't box the tuple's items. However, even if you manually boxed the items as well, it still wouldn't match. So the following code prints "Unknown":

match box (box o1, box o2) with
    | :? (BaseType * BaseType) -> printfn "BaseType"
    | _ -> printfn "Unknown"   // hit

This is because ConcreteType1 * ConcreteType2 is not a subtype of either BaseType * BaseType or obj * obj. Note that the following code won't even compile:

let testBad (tuple : BaseType * BaseType) =
    tuple :?> (ConcreteType1 * ConcreteType2)   // compiler error: can't cast

That means the following code will print "ConcreteType":

match box (o1, o2) with
    | :? (BaseType * BaseType) -> printfn "BaseType"
    | :? (ConcreteType1 * ConcreteType2) -> printfn "ConcreteType"   // hit
    | _ -> printfn "Unknown"

This is just the way that polymorphic types interact with OO subtyping. As another example, List<ConcreteType1> isn't a subtype of List<BaseType>, so no amount of boxing and casting will get them to match either.

The confusing thing is that F# does have a special case where it will automatically cast a ConcreteType1 * ConcreteType2 argument to match a BaseType * BaseType parameter:

let testGood (tuple : BaseType * BaseType) =
    printfn "good"

testGood (o1, o2)   // this works and prints "good"

I think this is because tupled arguments are common in other languages, so F# is trying to maintain compatibility, but I'm not certain.