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.
Comments
- Anonymous
January 13, 2010
Thanks for the information! I was wondering: I would expect AddPrintTransformer to return an option type instead of null. Or am I missing something?