Reputation: 45052
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
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
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
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