如何将printf样式的函数传递给F#中的另一个函数

Cha*_*lie 3 f# type-inference

我想在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?

kvb*_*kvb 6

问题是,当你说(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)


Tom*_*cek 5

我认为 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 抽象,因此它可能是一个不错的选择。