I want to demonstrate the ksprintf function, because that one accepts a continuation that will allow you to pass on the resulting string to e.g. a log system.
For the purpose of demonstration, let's first create something that can take a single string as input and pass it on, in this case to the console.
let writeStringToConsole (s: string) = Console.WriteLine ("OUTPUT : " + s)
So now, if writeStringToConsole is all we have, how to we make it accept F# formatting?
let printToConsole format = Printf.ksprintf writeStringToConsole format
Example that demonstrates that it works.
type DU = A | B
let i = 7
let s = "thirteen"
let du = B
printToConsole """an int %i and a string "%s" here""" i s
printToConsole """an int %i and a string "%s" and DU %A here""" i s du
// OUTPUT : an int 7 and a string "thirteen" here
// OUTPUT : an int 7 and a string "thirteen" and DU B here
// Note that OUTPUT is also part of the actual output,
// and it demonstrates how you can add e.g. a timestamp
// or line number or something to the output string, without
// it being part of the formatting.
edit: Some additional notes
The format string must be a literal. That's because the literal string must be read at compile time in order to compute the function that must be returned in order to gobble up whatever values/types that follow the format string.
For example, if you do printToConsole "%i %s %A %A" 7 "x" myType yourType
, then you'll see int -> string -> MyType -> YourType
in the signature of printToConsole
where it's used.
There is a way to use plain strings as format strings with this system, but I don't remember how it's done, and anyway it spoils the type safety. It comes in handy when doing internationalization of strings, and your format strings must come from a resource and not F# source due to external translator services.
edit 2 : Wrap e.g. log system
I created an interface to use for various logging systems, which pretty much share the same features.
type ILogger =
...
abstract member Debugf: StringFormat<'h, unit> -> 'h
abstract member Verbosef: StringFormat<'h, unit> -> 'h
abstract member Infof: StringFormat<'h, unit> -> 'h
abstract member Warningf: StringFormat<'h, unit> -> 'h
abstract member Errorf: StringFormat<'h, unit> -> 'h
abstract member Fatalf: StringFormat<'h, unit> -> 'h
Then an implementation for my currently used logging system looks like this.
type internal SiLogger(session: Session) =
let slogf = Printf.ksprintf
...
interface ILogger with
...
member _.Debugf format = slogf session.LogDebug format
member _.Verbosef format = slogf session.LogVerbose format
member _.Infof format = slogf session.LogMessage format
member _.Warningf format = slogf session.LogWarning format
member _.Errorf format = slogf session.LogError format
member _.Fatalf format = slogf session.LogFatal format
And there is a null logger.
let slogf = Printf.ksprintf
let dummyLog _ = () // The parameter is the title string.
let dummy format = slogf dummyLog format
let getNullLogger () =
{ new ILogger with
...
member _.Debugf format = dummy format
member _.Verbosef format = dummy format
member _.Infof format = dummy format
member _.Warningf format = dummy format
member _.Errorf format = dummy format
member _.Fatalf format = dummy format
...
}