Skip to content

Custom formatting of a record type is not consistent when nested within other types #12686

@christiandaley

Description

@christiandaley

The following code displays inconsistent behavior for how different types are formatted when printing:

[<StructuredFormatDisplayAttribute("Custom structured format")>]
type MyRecord = {
    a: int
} with
    override _.ToString() = "Custom ToString()"

    interface System.IFormattable with
        member _.ToString (_format, _provider) = "Custom IFormattable"

type OtherRecord = {
    b: MyRecord
}

type MyUnion = | MyUnion of MyRecord

[<EntryPoint>]
let main argv =
    let myRec = {
        a = 10
    }

    let otherRec = {
        b = myRec
    }

    let myUnion = MyUnion myRec

    printfn $"{myRec}"

    printfn $"{otherRec}"

    printfn $"{myUnion}"

    let myTuple = (myRec, myRec)

    printfn $"{myTuple}"

    0

ConsoleApp1.zip

Expected behavior
Ideally The above code would print

Custom IFormattable
{ b = { a = Custom IFormattable } }
MyUnion { a = Custom IFormattable }
(Custom IFormattable, Custom IFormattable)

OR

Custom ToString()
{ b = { a = Custom ToString() } }
MyUnion { a = Custom ToString() }
(Custom ToString(), Custom ToString())

OR

Custom structured format
{ b = Custom structured format }
MyUnion Custom structured format
(Custom structured format,Custom structured format)

In order for the formatting behavior of different types to be consistent.

Actual behavior

The above code prints

Custom IFormattable
{ b = Custom structured format }
MyUnion Custom structured format
(Custom ToString(), Custom ToString())

So it seems that direct string interpolation calls the System.IFormattable.ToString function. Tuple types on the other hand call
ToString() on each of their components. Record types and discriminated union types use the StructuredFormatDisplayAttribute. 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

[<StructuredFormatDisplayAttribute("Custom structured format")>]
type MyRecord = {
    a: int
} with
    override _.ToString() = "Custom ToString()"

    interface System.IFormattable with
        member this.ToString (_format, _provider) = this.ToString ()

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 the StructuredFormatDisplayAttribute 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

  • Operating system: Windows 10
  • .NET Runtime kind: .NET 5.0
  • F# 6.0.1
  • Visual Studio 2019 16.11.9

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions