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 (1)

  1. Thanks for the information!

    I was wondering: I would expect AddPrintTransformer to return an option type instead of null. Or am I missing something?