Monday, December 29, 2008

The F# library provides a variety of functions (based on the printf functions found in OCaml) that produce formatted text.

printf outputs formatted text to the console using the stdout stream.
printfn outputs formatted text suffixed with a new-line character to the console using the stdout stream.
eprintf outputs formatted text to the console using the stderr stream.
eprintfn outputs formatted text suffixed with a new-line character to the console using the stderr stream.
sprintf returns formatted text as a string.
bprintf appends formatted text to a System.Text.StringBuilder.
fprintf writes formatted text to a System.IO.TextWriter.
fprintfn writes formatted text suffixed with a new-line character to a System.IO.TextWriter.
twprintf writes formatted text to a System.IO.TextWriter.
twprintfn writes formatted text suffixed with a new-line character to a System.IO.TextWriter.

The functions above are especially powerful because the F# compiler will type-check the format arguments and give design-time errors.

Type-safe Format String Error with Tooltip

While this set of functions covers most of the cases where formatted text is needed, there is one glaring omission: debug output. If we want to pass formatted text to System.Diagnostics.Debug.Write, we have to do it using sprintf like so:

System.Diagnostics.Debug.Write(sprintf "The answer = %d" 42)

Obviously, that's not ideal. What we would really like is a function that behaves exactly like the other printf functions but writes debug output. Fortunately, extensibility has been built into the F# library, making it possible to create additional formatting functions that benefit from the same sweet type-checking. So, how do we go about doing that? The trick is to wrap our functions around a special F# library function, ksprintf.

ksprintf is similar to sprintf in that it formats text as a string. However, instead of returning that string to the caller, it passes the string into a continuation.

I haven't covered the broad topic of continuations yet because a) they can be pretty eye-crossing when presented plainly, and b) there are already fantastic treatments out there. The basic idea is to pass a lambda1 (the so-called continuation) into a computation that will be executed when the computation is finished. Really. That's it. This is a simple idea, but it can become complicated quickly. However, in the case of ksprintf it remains simple. We'll pass a lambda that calls Debug.Write with the final string after it has been formatted.

module Debug =
  open System.Diagnostics

  let writef fmt = Printf.ksprintf (fun s -> Debug.Write(s)) fmt

  let writefn fmt = Printf.ksprintf (fun s -> Debug.WriteLine(s)) fmt

In fact, the wrapper lambdas are redundant because Debug.Write and Debug.WriteLine have overloads that match the expected signature. We can simply pass the functions themselves and remove the wrappers:

module Debug =
  open System.Diagnostics

  let writef fmt = Printf.ksprintf Debug.Write fmt

  let writefn fmt = Printf.ksprintf Debug.WriteLine fmt

Just drop that code into an F# project, and you'll be writing debug output in a beautiful, concise F# style.

Debug.writef "The answer = %d" 42

You'll even get helpful design-time type errors:

Debug.writef Type-safe Format String Error with Tooltip

Now it's just a matter of convincing the F# team to add it to the libraries. ;-)

1Did It With .NET readers probably already know that "lambda" = "anonymous function".

posted on Monday, December 29, 2008 11:35:14 AM (Pacific Standard Time, UTC-08:00)  #    Comments [2]

kick it on DotNetKicks.com