How to write custom version of F# printfn that takes variable number of arguments?

Viewed 498

I want to write an extended version of F# printfn function that prints current time in addition to text. Something like this:

let printimefn fmt =
    let time = DateTime.Now
    let strtime = sprintf "%02d:%02d:%02d" time.Hour time.Minute time.Second
    printfn "%s %A"  strtime fmt

Unfortunately this doesn't work as expected. The "fmt" argument loses its type Printf.TextWriterFormat<'T>.

I can force its type by using type annotation:

let printimefn (fmt : Printf.TextWriterFormat<'T>) =
    let time = DateTime.Now
    let strtime = sprintf "%02d:%02d:%02d" time.Hour time.Minute time.Second
    printfn "%s %A"  strtime fmt

But then the result of printimefn becomes unit, and not 'T. So it still doesn't work. I wonder what is the right way of to write a custom printfn function.

2 Answers

This can be done with Printf.ksprintf

let printimefn fmt =
    let time = DateTime.Now
    Printf.ksprintf (
        fun s ->
            printfn "%02d:%02d:%02d %s" time.Hour time.Minute time.Second s)
        fmt

I want to demonstrate this by moving the reading of the time out of the function, for two reasons. Primarily to demonstrate how to add one or more arguments that are not consumed by ksprintf, but also because we should have a function that does not have side effects. If you want the time to be read inside the function, then instead create a wrapper function that reads the time, then calls the following function.

First there are two explicit arguments not passed on to ksprintf. Then follows implicitly the format string and any implicit arguments the format string requires.

let tsprintf (dateTime: DateTime) (tag: string) =
    let dateTimeString =
        dateTime.ToString (@"yyyy/MM/dd HH:mm:ss",
            System.Globalization.CultureInfo.InvariantCulture)
    Printf.ksprintf (fun formatString ->
        "[" + dateTimeString + " <" + tag + ">] " + formatString)

let testTime = DateTime(1987, 12, 31, 11, 59, 58)

let message = tsprintf testTime "007" "Hello %s!" "James Bond"

message.Dump() // LINQPad output : [1987/12/31 11:59:58 <007>] Hello James Bond!

The InvariantCulture in combination with the chosen time format string makes sure the result you see is exactly what's in the comment at the end, no matter where or what.

The reason I use DateTime.ToString, is that this formats DateTime whichever way you want. Formatting it in ksprintf doesn't seem like a good idea.

The arguments not consumed by ksprintf are explicitly declared in tsprintf. The format string and the following arguments for ksprintf are not declared explicitly, but they follow all explicitly declared arguments.

Related