F# has a formatting directive "%A" that is very powerful because triggers a formatter to expand types and list individual members. In some places in our application data are logged using ToString method (there are some technical reasons for that), and then for types like discriminated unions it's only a type name that is logged. Too bad so we started overriding ToString methods for some types.
To give you an example:
open System
type DiscrUnion =
| Text of string
let t1 = DiscrUnion.Text "text"
sprintf "%A" t1
sprintf "%s" <| t1.ToString()
type DiscrUnionWithToString =
| Text of string
override this.ToString() = sprintf "%A" this
let t2 = DiscrUnionWithToString.Text "text"
sprintf "%A" t2
sprintf "%s" <| t2.ToString()
DiscrUnion.ToString() is printed like "FSI_0003+DiscrUnion", but for DiscrUnionWithToString.ToString() I get the actual properties: Text "text".
So far so good. However, for CLR types such override causes a catastrophic result: stack overflow! Here is an example:
type PocoType() =
member val Text : string = null with get, set
let t3 = PocoType()
t3.Text <- "text"
sprintf "%A" t3
sprintf "%s" <| t3.ToString()
type PocoTypeWithToString() =
member val Text : string = null with get, set
override this.ToString() = sprintf "%A" this
let t4 = PocoTypeWithToString()
t4.Text <- "text"
sprintf "%A" t4
sprintf "%s" <| t4.ToString()
Don't even try to instantiate PocoTypeWithToString. StackOverflowException.
I understand that for POCO type an attempt to use "%A" formatting directive causes ToString call, so when ToString itself contains such directive it will fail. But what is the right way for ToString overrides? And should I beware only C# kind of types (discriminated unions and records seem to work fine), or there are other things to be aware of?
The reason why the StackOverflowException happen is because the printer uses GetValueInfoOfObject
to format. As you can see, if the object is an F# object, it has special cases for how to deal with them (tuples, functions, unions, exceptions, records).
However, if it isn't one of those cases, it will make it an ObjectValue(obj)
. Later on, in reprL
we have some special cases to deal with ObjectValue
s such as string, array, map/set, ienumerable, and then at the end if that fails, it will just make it a basic layout (let basicL = LayoutOps.objL obj
) of type Leaf
.
Much later, that Leaf
is formatted using leafformatter
. leafformatter
can deal with primitives, but when it deals with a complex object such as your POCO, it does let text = obj.ToString()
, which results in an infinite loop and the StackOverflow exception.
The solution is to not use %A
on POCOs.
The good news is that the next version of F# may have a default ToString
implementation for records/unions that is effectively override this.ToString() = sprintf "%A" this
. The implementation for it is partially complete here: https://github.com/Microsoft/visualfsharp/pull/1589. It may solve the problem you had to begin with.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With