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

3
我希望在F#中创建一个函数,该函数接受一个printf风格的函数作为参数,并使用该参数输出数据。使用方法类似于以下内容:
OutputStuff printfn

我的第一次尝试是让编译器为我解决所有问题:

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

那个失败了,因为它认为output是一个接受string并返回unit的函数,所以第二个调用失败了。
接下来,我尝试声明output具有与printfn相同的签名:
let OutputStuff (output : Printf.TextWriterFormat<'a> -> 'a) =
    output "Header"
    output "Data: %d" 42

这段代码失败了,因为编译器决定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

这个尝试失败,结果和之前的一样。

我该如何说服编译器不去特化 output 的类型,而是将其保留为 Printf.TextWriterFormat<'a> -> 'a 呢?


你真的需要传递类似printf的函数本身吗?如果你有一些想要使用的函数,它接受一个string并且你想用格式化的字符串调用它,只需使用Printf.ksprintf与你的函数。 - Jack P.
密切相关:https://dev59.com/5W035IYBdhLWcg3wMM-W - Charlie
1
你可能会发现这个很有用——在这个文件的底部(Pervasive.fs)有一些我编写的函数,它们接受任意格式字符串并在内部使用Printf.ksprintf - Jack P.
3个回答

7
问题在于当你说(output: Printf.TextWriterFormat<'a> -> 'a)时,这意味着“存在某个'a',使得输出将Printf.TextWriterFormat<'a>'转换为'a'”。相反,你想要表达的是“对于所有'a',输出可以将Printf.TextWriterFormat<'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 }

听起来像是F#的泛型上下文(a'和b')根据不同的上下文(泛型方法和泛型函数)有不同的解释,这对于来自C#和C++的人来说有点奇怪。感谢您解释了这个差异。 - Charlie

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

这样,您仍然可以使用"printf"风格的格式化字符串打印到不同的输出中,但代码看起来简单得多(或者,您可以使用Printf.kprintf并指定一个接受string的打印函数,而不是使用TextWriter)。如果您想要打印到内存中的字符串,那也很容易:
let sb = System.Text.StringBuilder()
OutputStuff (new System.IO.StringWriter(sb))
sb.ToString()

一般来说,TextWriter 是一个标准的 .NET 抽象,用于指定打印输出,因此它可能是一个不错的选择。


谢谢您的建议 - 我同意这可能比kvb提出的IPrinter方法更实用。很难决定接受哪一个,但我选择了kvb的建议,因为在OutputStuff内部的使用方式最接近我想要的。 - Charlie

1

使用 FSharp 的 inline 特性,您可以预先配置 Printf.ksprintf 函数的 "work" 函数,从而获得一个最终函数,该函数接受格式字符串以及其特殊的必需参数,类似于 printfsprintf。接受该结果 string 的 work 函数可以做任何它想做的事情,例如在此日志示例中将 string 打印到 console

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

使用示例:

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)

...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接