F# Interactive Tips and Tricks: Formatting Data using AddPrinter, AddPrintTransformer and %A in sprintf/printf/fprintf

Mingtian Ni asked the following:

I ‘d like to change the output format for certain types, especially collection types, in fsi. What are the reasonable ways for this? ... Can somebody give a few references here? Or even better with guidelines and working examples.

Here are some tips and tricks for formatting data in F# Interactive. This is not meant to be a comprehensive guide, just enough to get you started. Please let me know if you need more examples.

 

For F# Interactive, one easy one is to use fsi.AddPrintTransformer to generate a surrogate display object (or use fsi.AddPrinter which is similar, but where you generate a string), e.g.

type C(elems:int list) =

member x.Contents = elems

member x.IsBig = elems.Length > 100

let c = C [1;2;3]

fsi.AddPrintTransformer (fun (c:C) -> box c.Contents)

producing

val c : C = [1; 2; 3]

One nice thing about AddPrintTransformer is you can make it conditional, returning null to indicate that the formatter should be skipped:

fsi.AddPrintTransformer (fun (c:C) -> if c.IsBig then null else box c.Contents)

Which is particularly nice if you use it with the “obj” type as you can do very specific custom formatting on any object:

fsi.AddPrintTransformer (fun (obj:obj) -> match obj with :? C as c -> box c.Contents | _ -> null)

One problem with this is that fsi.AddPrinter and fsi.AddPrintTransformer don’t modify the behaviour of the %A formats in sprintf, printf etc. For those there is a limited facility to put a simple attribute on a type which names a property generating a surrogate object, along with some surrounding text:

[<StructuredFormatDisplayAttribute("CCC {Contents}")>]

type C(elems:int list) =

member x.Contents = elems

let c = C [1;2;3]

producing

val c : C = CCC [1; 2; 3]

If your type is a generic collection type, then use a list or sequence as the surrogate object.

If your type is a matrix or table type, then use a 2D array as the surrogate object.

If your type is logically a union type, but you've hidden its representation behind an abstraction boundary, then consider using a separate helper union type which unwraps the structure of your object (i.e. unwraps it one level if your type is a recursive type)

If your data is recursively tree structured you can represent the children as a list:

[<StructuredFormatDisplayAttribute("Tree {Contents}")>]

type Tree(node: int, elems: Tree list) =

member x.Contents = (node, elems)

let c = Tree (1, [ Tree (2, []); Tree (3, [ Tree (4, []) ]) ])

let c2 = Tree (1, [ c; c])

let c3 = Tree (1, [ c2; c2])

Producing the pleasing:

val c3 : Tree =

Tree (1,

[Tree (1,

[Tree (1, [Tree (2, []); Tree (3, [Tree (4, [])])]);

Tree (1, [Tree (2, []); Tree (3, [Tree (4, [])])])]);

Tree (1,

[Tree (1, [Tree (2, []); Tree (3, [Tree (4, [])])]);

Tree (1, [Tree (2, []); Tree (3, [Tree (4, [])])])])])

You should generally also consider implementing ToString, and consider adding a DebuggerDisplay attribute if you’re using the VS debugger a lot.