Plain text formatting
F# supports type-checked formatting of plain text using printf
, printfn
, sprintf
, and related functions.
For example,
dotnet fsi
> printfn "Hello %s, %d + %d is %d" "world" 2 2 (2+2);;
gives the output
Hello world, 2 + 2 is 4
F# also allows structured values to be formatted as plain text. For example, consider the following example that formats the output as a matrix-like display of tuples.
dotnet fsi
> printfn "%A" [ for i in 1 .. 5 -> [ for j in 1 .. 5 -> (i, j) ] ];;
[[(1, 1); (1, 2); (1, 3); (1, 4); (1, 5)];
[(2, 1); (2, 2); (2, 3); (2, 4); (2, 5)];
[(3, 1); (3, 2); (3, 3); (3, 4); (3, 5)];
[(4, 1); (4, 2); (4, 3); (4, 4); (4, 5)];
[(5, 1); (5, 2); (5, 3); (5, 4); (5, 5)]]
Structured plain text formatting is activated when you use the %A
format in printf
formatting strings.
It's also activated when formatting the output of values in F# interactive, where the output includes extra information and is additionally customizable.
Plain text formatting is also observable through any calls to x.ToString()
on F# union and record values, including those
that occur implicitly in debugging, logging, and other tooling.
Checking of printf
-format strings
A compile-time error will be reported if a printf
formatting function is used with an argument that doesn't match the printf format
specifiers in the format string. For example,
sprintf "Hello %s" (2+2)
gives the output
sprintf "Hello %s" (2+2)
----------------------^
stdin(3,25): error FS0001: The type 'string' does not match the type 'int'
Technically speaking, when using printf
and other related functions, a special rule in the F# compiler
checks the string literal passed as the format string, ensuring the subsequent arguments applied are of
the correct type to match the format specifiers used.
Format specifiers for printf
Format specifications for printf
formats are strings with %
markers that indicate format. Format placeholders consist of %[flags][width][.precision][type]
where the type is interpreted as follows:
Format specifier | Type(s) | Remarks |
---|---|---|
%b |
bool (System.Boolean ) |
Formatted as true or false |
%s |
string (System.String ) |
Formatted as its unescaped contents |
%c |
char (System.Char ) |
Formatted as the character literal |
%d , %i |
a basic integer type | Formatted as a decimal integer, signed if the basic integer type is signed |
%u |
a basic integer type | Formatted as an unsigned decimal integer |
%x , %X |
a basic integer type | Formatted as an unsigned hexadecimal number (a-f or A-F for hex digits respectively) |
%o |
a basic integer type | Formatted as an unsigned octal number |
%B |
a basic integer type | Formatted as an unsigned binary number |
%e , %E |
a basic floating point type | Formatted as a signed value having the form [-]d.dddde[sign]ddd where d is a single decimal digit, dddd is one or more decimal digits, ddd is exactly three decimal digits, and sign is + or - |
%f , %F |
a basic floating point type | Formatted as a signed value having the form [-]dddd.dddd , where dddd is one or more decimal digits. The number of digits before the decimal point depends on the magnitude of the number, and the number of digits after the decimal point depends on the requested precision. |
%g , %G |
a basic floating point type | Formatted using as a signed value printed in %f or %e format, whichever is more compact for the given value and precision. |
%M |
a decimal (System.Decimal ) value |
Formatted using the "G" format specifier for System.Decimal.ToString(format) |
%O |
any value | Formatted by boxing the object and calling its System.Object.ToString() method |
%A |
any value | Formatted using structured plain text formatting with the default layout settings |
%a |
any value | Requires two arguments: a formatting function accepting a context parameter and the value, and the particular value to print |
%t |
any value | Requires one argument: a formatting function accepting a context parameter that either outputs or returns the appropriate text |
%% |
(none) | Requires no arguments and prints a plain percent sign: % |
Basic integer types are byte
(System.Byte
), sbyte
(System.SByte
), int16
(System.Int16
), uint16
(System.UInt16
), int32
(System.Int32
), uint32
(System.UInt32
), int64
(System.Int64
), uint64
(System.UInt64
), nativeint
(System.IntPtr
), and unativeint
(System.UIntPtr
).
Basic floating point types are float
(System.Double
), float32
(System.Single
), and decimal
(System.Decimal
).
The optional width is an integer indicating the minimal width of the result. For instance, %6d
prints an integer, prefixing it with spaces
to fill at least six characters. If width is *
, then an extra integer argument is taken to specify the corresponding width.
Valid flags are:
Flag | Effect |
---|---|
0 |
Add zeros instead of spaces to make up the required width |
- |
Left justify the result within the specified width |
+ |
Add a + character if the number is positive (to match a - sign for negatives) |
space character | Add an extra space if the number is positive (to match a '-' sign for negatives) |
The printf #
flag is invalid and a compile-time error will be reported if it is used.
Values are formatted using invariant culture. Culture settings are irrelevant to printf
formatting except
when they affect the results of %O
and %A
formatting. For more information, see structured plain text formatting.
%A
formatting
The %A
format specifier is used to format values in a human-readable way, and can also be useful for reporting diagnostic information.
Primitive values
When formatting plain text using the %A
specifier, F# numeric values are formatted
with their suffix and invariant culture. Floating point values are formatted using
10 places of floating point precision. For example,
printfn "%A" (1L, 3n, 5u, 7, 4.03f, 5.000000001, 5.0000000001)
produces
(1L, 3n, 5u, 7, 4.03000021f, 5.000000001, 5.0)
When using the %A
specifier, strings are formatted using quotes. Escape codes are not added and instead the raw characters are printed. For example,
printfn "%A" ("abc", "a\tb\nc\"d")
produces
("abc", "a b
c"d")
.NET values
When formatting plain text using the %A
specifier, non-F# .NET objects are formatted by using x.ToString()
using the default settings of .NET given by
System.Globalization.CultureInfo.CurrentCulture
and System.Globalization.CultureInfo.CurrentUICulture
. For example,
open System.Globalization
let date = System.DateTime(1999, 12, 31)
CultureInfo.CurrentCulture <- CultureInfo.GetCultureInfo("de-DE")
printfn "Culture 1: %A" date
CultureInfo.CurrentCulture <- CultureInfo.GetCultureInfo("en-US")
printfn "Culture 2: %A" date
produces
Culture 1: 31.12.1999 00:00:00
Culture 2: 12/31/1999 12:00:00 AM
Structured values
When formatting plain text using the %A
specifier, block indentation is used for F# lists and tuples. This is shown in the previous example.
The structure of arrays is also used, including multi-dimensional arrays. Single-dimensional arrays are shown with [| ... |]
syntax. For example,
printfn "%A" [| for i in 1 .. 20 -> (i, i*i) |]
produces
[|(1, 1); (2, 4); (3, 9); (4, 16); (5, 25); (6, 36); (7, 49); (8, 64); (9, 81);
(10, 100); (11, 121); (12, 144); (13, 169); (14, 196); (15, 225); (16, 256);
(17, 289); (18, 324); (19, 361); (20, 400)|]
The default print width is 80. This width can be customized by using a print width in the format specifier. For example,
printfn "%10A" [| for i in 1 .. 5 -> (i, i*i) |]
printfn "%20A" [| for i in 1 .. 5 -> (i, i*i) |]
printfn "%50A" [| for i in 1 .. 5 -> (i, i*i) |]
produces
[|(1, 1);
(2, 4);
(3, 9);
(4, 16);
(5, 25)|]
[|(1, 1); (2, 4);
(3, 9); (4, 16);
(5, 25)|]
[|(1, 1); (2, 4); (3, 9); (4, 16); (5, 25)|]
Specifying a print width of 0 results in no print width being used. A single line of text will result, except where embedded strings in the output contain line breaks. For example
printfn "%0A" [| for i in 1 .. 5 -> (i, i*i) |]
printfn "%0A" [| for i in 1 .. 5 -> "abc\ndef" |]
produces
[|(1, 1); (2, 4); (3, 9); (4, 16); (5, 25)|]
[|"abc
def"; "abc
def"; "abc
def"; "abc
def"; "abc
def"|]
A depth limit of 4 is used for sequence (IEnumerable
) values, which are shown as seq { ...}
. A depth limit of 100 is used for list and array values.
For example,
printfn "%A" (seq { for i in 1 .. 10 -> (i, i*i) })
produces
seq [(1, 1); (2, 4); (3, 9); (4, 16); ...]
Block indentation is also used for the structure of public record and union values. For example,
type R = { X : int list; Y : string list }
printfn "%A" { X = [ 1;2;3 ]; Y = ["one"; "two"; "three"] }
produces
{ X = [1; 2; 3]
Y = ["one"; "two"; "three"] }
If %+A
is used, then the private structure of records and unions
is also revealed by using reflection. For example
type internal R =
{ X : int list; Y : string list }
override _.ToString() = "R"
let internal data = { X = [ 1;2;3 ]; Y = ["one"; "two"; "three"] }
printfn "external view:\n%A" data
printfn "internal view:\n%+A" data
produces
external view:
R
internal view:
{ X = [1; 2; 3]
Y = ["one"; "two"; "three"] }
Large, cyclic, or deeply nested values
Large structured values are formatted to a maximum overall object node count of 10000.
Deeply nested values are formatted to a depth of 100. In both cases ...
is used
to elide some of the output. For example,
type Tree =
| Tip
| Node of Tree * Tree
let rec make n =
if n = 0 then
Tip
else
Node(Tip, make (n-1))
printfn "%A" (make 1000)
produces a large output with some parts elided:
Node(Tip, Node(Tip, ....Node (..., ...)...))
Cycles are detected in the object graphs and ...
is used at places where cycles are detected. For example
type R = { mutable Links: R list }
let r = { Links = [] }
r.Links <- [r]
printfn "%A" r
produces
{ Links = [...] }
Lazy, null, and function values
Lazy values are printed as Value is not created
or equivalent text when the value has not yet been evaluated.
Null values are printed as null
unless the static type of the value is determined to be a union type where null
is
a permitted representation.
F# function values are printed as their internally generated closure name, for example, <fun:it@43-7>
.
Customize plain text formatting with StructuredFormatDisplay
When using the %A
specifier, the presence of the StructuredFormatDisplay
attribute on type declarations is respected. This can
be used to specify surrogate text and property to display a value. For example:
[<StructuredFormatDisplay("Counts({Clicks})")>]
type Counts = { Clicks:int list}
printfn "%20A" {Clicks=[0..20]}
produces
Counts([0; 1; 2; 3;
4; 5; 6; 7;
8; 9; 10; 11;
12; 13; 14;
15; 16; 17;
18; 19; 20])
Customize plain text formatting by overriding ToString
The default implementation of ToString
is observable in F# programming. Often, the default results
aren't suitable for use in either programmer-facing information display or user output, and as a result it
is common to override the default implementation.
By default, F# record and union types override the implementation of ToString
with an implementation that
uses sprintf "%+A"
. For example,
type Counts = { Clicks:int list }
printfn "%s" ({Clicks=[0..10]}.ToString())
produces
{ Clicks = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10] }
For class types, no default implementation of ToString
is provided and the .NET default is used, which reports the
name of the type. For example,
type MyClassType(clicks: int list) =
member _.Clicks = clicks
let data = [ MyClassType([1..5]); MyClassType([1..5]) ]
printfn "Default structured print gives this:\n%A" data
printfn "Default ToString gives:\n%s" (data.ToString())
produces
Default structured print gives this:
[MyClassType; MyClassType]
Default ToString gives:
[MyClassType; MyClassType]
Adding an override for ToString
can give better formatting.
type MyClassType(clicks: int list) =
member _.Clicks = clicks
override _.ToString() = sprintf "MyClassType(%0A)" clicks
let data = [ MyClassType([1..5]); MyClassType([1..5]) ]
printfn "Now structured print gives this:\n%A" data
printfn "Now ToString gives:\n%s" (data.ToString())
produces
Now structured print gives this:
[MyClassType([1; 2; 3; 4; 5]); MyClassType([1; 2; 3; 4; 5])]
Now ToString gives:
[MyClassType([1; 2; 3; 4; 5]); MyClassType([1; 2; 3; 4; 5])]
Customize plain text formatting with StructuredFormatDisplay
and ToString
To achieve consistent formatting for %A
and %O
format specifiers, combine the use of StructuredFormatDisplay
with an override of ToString
. For example,
[<StructuredFormatDisplay("{DisplayText}")>]
type MyRecord =
{
a: int
}
member this.DisplayText = this.ToString()
override _.ToString() = "Custom ToString"
Evaluating the following definitions
let myRec = { a = 10 }
let myTuple = (myRec, myRec)
let s1 = sprintf $"{myRec}"
let s2 = sprintf $"{myTuple}"
let s3 = sprintf $"%A{myTuple}"
let s4 = sprintf $"{[myRec; myRec]}"
let s5 = sprintf $"%A{[myRec; myRec]}"
gives the text
val myRec: MyRecord = Custom ToString
val myTuple: MyRecord * MyRecord = (Custom ToString, Custom ToString)
val s1: string = "Custom ToString"
val s2: string = "(Custom ToString, Custom ToString)"
val s3: string = "(Custom ToString, Custom ToString)"
val s4: string = "[Custom ToString; Custom ToString]"
val s5: string = "[Custom ToString; Custom ToString]"
The use of StructuredFormatDisplay
with the supporting DisplayText
property means the fact that the myRec
is a structural record type is ignored during structured printing, and the override of ToString()
is preferred in all circumstances.
An implementation of the System.IFormattable
interface can be added for further customization in the presence of .NET format specifications.
F# Interactive structured printing
F# Interactive (dotnet fsi
) uses an extended version of structured plain text formatting to
report values and allows additional customizability. For more information, see F# Interactive.
Customize debug displays
Debuggers for .NET respect the use of attributes such as DebuggerDisplay
and DebuggerTypeProxy
,
and these affect the structured display of objects in debugger inspection windows.
The F# compiler automatically generated these attributes for discriminated union and record types, but
not class, interface, or struct types.
These attributes are ignored in F# plain text formatting, but it can be useful to implement these methods to improve displays when debugging F# types.
See also
Feedback
https://aka.ms/ContentUserFeedback.
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see:Submit and view feedback for