Charlie
Charlie

Reputation: 45052

How to pass a printf-style function to another function in F#

I'd like to make a function in F# that accepts a printf-style function as an argument, and uses that argument to output data. Usage would be something like the following:

OutputStuff printfn

My first attempt was to let the compiler figure it all out for me:

let OutputStuff output =
    output "Header"
    output "Data: %d" 42

That fails because it decides that output is a function taking string and returning unit, so the second call fails.

Next I tried declaring output to have the same signature as printfn:

let OutputStuff (output : Printf.TextWriterFormat<'a> -> 'a) =
    output "Header"
    output "Data: %d" 42

This fails because the compiler decides that the real type of output is Printf.TextWriterFormat<string> -> unit, so again the second call fails. It also generates warning FS0064 indicating that the first call to output causes the code to be less generic than the type annotations, which is the crux of the issue here.

Last, I tried declaring the output function as a separate type abbreviation:

type OutputMe<'a> = Printf.TextWriterFormat<'a> -> 'a
let OutputStuff (output : OutputMe<'a>) =
    output "Header"
    output "Data: %d" 42

This fails with the same results as the previous attempt.

How do I convince the compiler to not specialize the type of output and leave it as Printf.TextWriterFormat<'a> -> 'a?

Upvotes: 3

Views: 656

Answers (3)

George
George

Reputation: 2801

Using the inline feature of FSharp you can pre-configure the Printf.ksprintf function with a "work" function and thereby have a final function that accepts a format string together with its peculiar required parameters similar to printf or sprintf. The work function, which accepts that resulting string, can presumably do whatever it wants such as in the case of this logging example which prints the string to the console:

let logger = fun (msg:string) -> System.Console.WriteLine msg
let inline log msg = Printf.ksprintf logger msg

Usage examples:

open System
open System.Globalization.CultureInfo.CurrentCulture

log "Hello %s, how is your %s" name Calendar.GetDayOfWeek(DateTime.Today)

log "132 + 6451 = %d" (132+6451)

...

Upvotes: 1

Tomas Petricek
Tomas Petricek

Reputation: 243051

I think the answer by kvb is a great explanation of the problem - why is it difficult to pass printf like functions to other functions as parameter. While kvb gives a workaround that makes this possible, I think it is probably not very practical (because the use of interfaces makes it a bit complex).

So, if you want to parameterize your output, I think it is easier to take System.IO.TextWriter as an argument and then use printf like function that prints the output to the specified TextWriter:

let OutputStuff printer =
  Printf.fprintfn printer "Hi there!"
  Printf.fprintfn printer "The answer is: %d" 42

OutputStuff System.Console.Out

This way, you can still print to different outputs using the printf style formatting strings, but the code looks a lot simpler (alternatively, you could use Printf.kprintf and specify a printing function that takes string instead of using TextWriter).

If you want to print to an in-memory string, that's easy too:

let sb = System.Text.StringBuilder()
OutputStuff (new System.IO.StringWriter(sb))
sb.ToString()

In general, TextWriter is a standard .NET abstraction for specifying printing output, so it is probably a good choice.

Upvotes: 5

kvb
kvb

Reputation: 55184

The problem is that when you say (output : Printf.TextWriterFormat<'a> -> 'a), that means "there is some 'a for which output takes a Printf.TextWriterFormat<'a> to an 'a". Instead, what you want to say is "for all 'a output can take a Printf.TextWriterFormat<'a> and return a 'a.

This is a bit ugly to express in F#, but the way to do it is with a type with a generic method:

type IPrinter =
    abstract Print : Printf.TextWriterFormat<'a> -> 'a

let OutputStuff (output : IPrinter) =
    output.Print "Header"
    output.Print "Data: %d" 42

OutputStuff { new IPrinter with member this.Print(s) = printfn s }

Upvotes: 7

Related Questions