我想在F#中创建一个接受printf样式函数作为参数的函数,并使用该参数输出数据.用法如下:
OutputStuff printfn
Run Code Online (Sandbox Code Playgroud)
我的第一次尝试是让编译器为我解决这个问题:
let OutputStuff output =
output "Header"
output "Data: %d" 42
Run Code Online (Sandbox Code Playgroud)
这失败是因为它决定这output是一个函数获取string和返回unit,所以第二次调用失败.
接下来,我尝试声明output具有相同的签名printfn:
let OutputStuff (output : Printf.TextWriterFormat<'a> -> 'a) =
output "Header"
output "Data: %d" 42
Run Code Online (Sandbox Code Playgroud)
这会失败,因为编译器决定实际类型output是Printf.TextWriterFormat<string> -> unit,所以第二次调用失败.它还会生成警告FS0064,指示第一次调用output导致代码不如类型注释更通用,这是问题的症结所在.
最后,我尝试将输出函数声明为单独的类型缩写:
type OutputMe<'a> = Printf.TextWriterFormat<'a> -> 'a
let OutputStuff (output : OutputMe<'a>) =
output "Header"
output "Data: %d" 42
Run Code Online (Sandbox Code Playgroud)
这失败的结果与之前的尝试相同.
如何说服编译器不专门化类型output并将其保留为Printf.TextWriterFormat<'a> -> 'a?
问题是,当你说(output : Printf.TextWriterFormat<'a> -> 'a),这意味着" 有一些'a针对输出需要Printf.TextWriterFormat<'a>一个'a".相反,你想要说的是" 对于所有 'a输出都可以采取Printf.TextWriterFormat<'a>并返回a 'a.
在F#中表达这有点难看,但是这样做的方法是使用带有泛型方法的类型:
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 }
Run Code Online (Sandbox Code Playgroud)
我认为 kvb 的答案很好地解释了这个问题——为什么将printf类似的函数作为参数传递给其他函数很困难。虽然 kvb 提供了一种解决方法,使这成为可能,但我认为它可能不是很实用(因为接口的使用使它有点复杂)。
所以,如果你想参数化你的输出,我认为它更容易System.IO.TextWriter作为一个参数,然后使用printf类似的函数将输出打印到指定的TextWriter:
let OutputStuff printer =
Printf.fprintfn printer "Hi there!"
Printf.fprintfn printer "The answer is: %d" 42
OutputStuff System.Console.Out
Run Code Online (Sandbox Code Playgroud)
这样,您仍然可以使用printf样式格式化字符串打印到不同的输出,但代码看起来要简单得多(或者,您可以使用Printf.kprintf并指定一个带string而不是 using的打印函数TextWriter)。
如果你想打印到内存中的字符串,那也很简单:
let sb = System.Text.StringBuilder()
OutputStuff (new System.IO.StringWriter(sb))
sb.ToString()
Run Code Online (Sandbox Code Playgroud)
一般来说,TextWriter是用于指定打印输出的标准 .NET 抽象,因此它可能是一个不错的选择。