The project I use contains a lot of classes inherited from single base class. In unit tests I need to compare received results by types and data.
When I use match comparison by types in the case when conditions list contains about enough many different conditions compiler throws OutOfMemoryException.
For example, following F# code raises System.OutOfMemoryException (parameter error FS0193) during compilation (and compilation took about 30 seconds before exception was thrown)
type IBaseClass() = class end
type IChildClass1 () = inherit IBaseClass ()
type IChildClass2 () = inherit IBaseClass ()
type IChildClass3 () = inherit IBaseClass ()
type IChildClass4 () = inherit IBaseClass ()
type IChildClass5 () = inherit IBaseClass ()
type IChildClass6 () = inherit IBaseClass ()
type IChildClass7 () = inherit IBaseClass ()
type IChildClass8 () = inherit IBaseClass ()
type IChildClass9 () = inherit IBaseClass ()
type IChildClass10 () = inherit IBaseClass ()
type IChildClass11 () = inherit IBaseClass ()
type IChildClass12 () = inherit IBaseClass ()
type IChildClass13 () = inherit IBaseClass ()
type IChildClass14 () = inherit IBaseClass ()
type IChildClass15 () = inherit IBaseClass ()
type IChildClass16 () = inherit IBaseClass ()
type IChildClass17 () = inherit IBaseClass ()
type IChildClass18 () = inherit IBaseClass ()
type IChildClass19 () = inherit IBaseClass ()
type IChildClass20 () = inherit IBaseClass ()
let AreEqual (original: IBaseClass) (compareWith: IBaseClass) : bool =
match (original, compareWith) with
| (:? IChildClass1 as a), (:? IChildClass1 as b) -> a = b
| (:? IChildClass2 as a), (:? IChildClass2 as b) -> a = b
| (:? IChildClass3 as a), (:? IChildClass3 as b) -> a = b
| (:? IChildClass4 as a), (:? IChildClass4 as b) -> a = b
| (:? IChildClass5 as a), (:? IChildClass5 as b) -> a = b
| (:? IChildClass6 as a), (:? IChildClass6 as b) -> a = b
| (:? IChildClass7 as a), (:? IChildClass7 as b) -> a = b
| (:? IChildClass8 as a), (:? IChildClass8 as b) -> a = b
| (:? IChildClass9 as a), (:? IChildClass9 as b) -> a = b
| (:? IChildClass10 as a), (:? IChildClass10 as b) -> a = b
| (:? IChildClass11 as a), (:? IChildClass11 as b) -> a = b
| (:? IChildClass12 as a), (:? IChildClass12 as b) -> a = b
| (:? IChildClass13 as a), (:? IChildClass13 as b) -> a = b
| (:? IChildClass14 as a), (:? IChildClass14 as b) -> a = b
| (:? IChildClass15 as a), (:? IChildClass15 as b) -> a = b
| (:? IChildClass16 as a), (:? IChildClass16 as b) -> a = b
| (:? IChildClass17 as a), (:? IChildClass17 as b) -> a = b
| (:? IChildClass18 as a), (:? IChildClass18 as b) -> a = b
| (:? IChildClass19 as a), (:? IChildClass19 as b) -> a = b
| (:? IChildClass20 as a), (:? IChildClass20 as b) -> a = b
| _ -> false
For sure I can add IEquatable interface to my IBaseClass class that's will avoid usage of such match construction, or add int Kind member (or enum) to the IBaseClass interface and make match not by types, but by some int value.
Note, I've tried to compile the same project in MS VS 2010 and MSVS 11 Beta, and had the same compiler error
Question: Why the compiler's OutOfMemoryException happens in my case (is it known compiler bug or other limitation), how should I reorganize my match condition to avoid it?
Update When I put classes into discriminated unions and use similar match comparison Fsc.exe compiles the project without exception
type AllClasses =
| ChildClass1 of IChildClass1 | ChildClass2 of IChildClass2 | ChildClass3 of IChildClass3 | ChildClass4 of IChildClass4 | ChildClass5 of IChildClass5 | ChildClass6 of IChildClass6
| ChildClass7 of IChildClass7 | ChildClass8 of IChildClass8 | ChildClass9 of IChildClass9 | ChildClass10 of IChildClass10 | ChildClass11 of IChildClass11 | ChildClass12 of IChildClass12
| ChildClass13 of IChildClass13 | ChildClass14 of IChildClass14 | ChildClass15 of IChildClass15 | ChildClass16 of IChildClass16 | ChildClass17 of IChildClass17 | ChildClass18 of IChildClass18
| ChildClass19 of IChildClass19 | ChildClass20 of IChildClass20
let AreEqual2 (original: AllClasses) (compareWith: AllClasses) : bool =
match (original, compareWith) with
| ChildClass1(a), ChildClass1(b) -> a = b
| ChildClass2(a), ChildClass2(b) -> a = b
| ChildClass3(a), ChildClass3(b) -> a = b
| ChildClass4(a), ChildClass4(b) -> a = b
| ChildClass5(a), ChildClass5(b) -> a = b
| ChildClass6(a), ChildClass6(b) -> a = b
| ChildClass7(a), ChildClass7(b) -> a = b
| ChildClass8(a), ChildClass8(b) -> a = b
| ChildClass9(a), ChildClass9(b) -> a = b
| ChildClass10(a), ChildClass10(b) -> a = b
| ChildClass11(a), ChildClass11(b) -> a = b
| ChildClass12(a), ChildClass12(b) -> a = b
| ChildClass13(a), ChildClass13(b) -> a = b
| ChildClass14(a), ChildClass14(b) -> a = b
| ChildClass15(a), ChildClass15(b) -> a = b
| ChildClass16(a), ChildClass16(b) -> a = b
| ChildClass17(a), ChildClass17(b) -> a = b
| ChildClass18(a), ChildClass18(b) -> a = b
| ChildClass19(a), ChildClass19(b) -> a = b
| ChildClass20(a), ChildClass20(b) -> a = b
| _ -> false
Thanks
This is caused by how the F# compiler compiles pattern matching on tuples in this case. I'm not entirely sure when exactly you run in this particular problem and when the compiler uses other approach, but here is an explanation why it fails in this case...
If you write a pattern matching like the one in your example, the compiler essentially generates a decision tree that tests the first pattern for
original
(:? IChildClass1
) and then generates two branches. The first branch checks whethercompareWith
is alsoIChildClass1
. If yes, it runs the first case. The rest of the pattern matching is then duplicated in both of the branches, so you get something like (you can check this by looking at the compiled code for smaller number of cases using ILSpy):This means that the size of the generated code is exponentially proportional to the number of cases in this pattern matching. For 20 of cases, the compiler tries to create 2^20 branches, which (unsurprisingly) fails.