Custom formatting of a record type is not consistent when nested within other types #12688
Replies: 5 comments 3 replies
-
Yes, this is really unfortunate and I agree it's inconsistent. However there's not a lot we can do about it
|
Beta Was this translation helpful? Give feedback.
-
@christiandaley Could you see if this formulation works for you to get the consistency you're after? There are still some issues with this - the recursive invocations of structured formatting can be problematic for stack depth - and some formatting context can be lost by recursive calls. type MyRecord =
{ a: int }
override x.ToString() = sprintf "%+A" x
type OtherRecord =
{ b: MyRecord }
type MyUnion = | MyUnion of MyRecord
let myRec = { a = 10 }
let otherRec = { b = myRec }
let myUnion = MyUnion myRec
let s1 = sprintf $"{myRec}"
let s2 = sprintf $"{otherRec}"
let s3 = sprintf $"{myUnion}"
let myTuple = (myRec, myRec)
let s4 = sprintf $"{myTuple}"
I get val s1: string = "{ a = 10 }"
val s2: string = "{ b = { a = 10 } }"
val s3: string = "MyUnion { a = 10 }"
val myTuple: MyRecord * MyRecord = ({ a = 10 }, { a = 10 })
val s4: string = "({ a = 10 }, { a = 10 })" |
Beta Was this translation helpful? Give feedback.
-
@dsyme If I use
Then it prints out
So this approach seems to work for both string interpolation and tuples. Unfortunately it still doesn't work for record types and unions. I didn't consider that .NET would make it difficult to accomplish this, though it does make sense because F# needs to be compatible with what C# is doing. Thanks for your response. |
Beta Was this translation helpful? Give feedback.
-
So part of the problem is you're not using Try this: [<StructuredFormatDisplayAttribute("{DisplayText}")>]
type MyRecord =
{ a: int }
member x.DisplayText = $"CustomA({x.a})"
[<StructuredFormatDisplayAttribute("{DisplayText}")>]
type OtherRecord =
{ b: MyRecord }
member x.DisplayText = $"CustomB(%A{x.b})"
type MyUnion = | MyUnion of MyRecord
let myRec = {
a = 10
}
let otherRec = {
b = myRec
} This gives val myRec: MyRecord = CustomA(10)
val otherRec: OtherRecord = CustomB(CustomA(10))
val myUnion: MyUnion = MyUnion CustomA(10)
val s1: string = "CustomA(10)"
val s2: string = "CustomB(CustomA(10))"
val s3: string = "MyUnion CustomA(10)"
val myTuple: MyRecord * MyRecord = (CustomA(10), CustomA(10))
val s4: string = "(CustomA(10), CustomA(10))" |
Beta Was this translation helpful? Give feedback.
-
@dsyme So if I do this
Then I get the following output
The |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
The following code displays inconsistent behavior for how different types are formatted when printing:
ConsoleApp1.zip
Expected behavior
Ideally The above code would print
OR
OR
In order for the formatting behavior of different types to be consistent.
Actual behavior
The above code prints
So it seems that direct string interpolation calls the
System.IFormattable.ToString
function. Tuple types on the other hand callToString()
on each of their components. Record types and discriminated union types use theStructuredFormatDisplayAttribute
. This behavior is inconsistent and results in a headache when you want to define a custom way for your record type to be printed, and want to have it "just work" regardless of the context in which it is being printed.Known workarounds
Doing this
Will bring the direct string interpolation and tuple printing in line with each other, but does not solve the issue for nested record types and discriminated unions. Passing something like
"{ToString()}"
to theStructuredFormatDisplayAttribute
does not work. At runtime it results in<StructuredFormatDisplay exception: Method 'Program+MyRecord.this.ToString()' not found.>
.I cannot find any way to solve the problem I am trying to solve, which is that any time an instance of
MyRecord
is printed, it should always result in the exact same string, regardless of whether it is printed directly, or printed because it's a member of another record that's being printed, or printed because it's a member of a record that's inside a tuple that's inside another record that's inside of a DU case that's inside of a tuple that's....etc.Related information
Beta Was this translation helpful? Give feedback.
All reactions