【问题标题】:How to pass a printf-style function to another function in F#如何将 printf 样式的函数传递给 F# 中的另一个函数
【发布时间】:2013-03-30 20:46:33
【问题描述】:

我想在 F# 中创建一个函数,它接受 printf 样式的函数作为参数,并使用该参数输出数据。用法如下所示:

OutputStuff printfn

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

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

失败是因为它认为output 是一个接受string 并返回unit 的函数,因此第二次调用失败。

接下来我尝试声明outputprintfn 具有相同的签名:

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

这失败了,因为编译器确定output 的真实类型是Printf.TextWriterFormat&lt;string&gt; -&gt; unit,所以第二次调用再次失败。它还会生成警告 FS0064,表明第一次调用 output 会导致代码没有类型注释那么通用,这是问题的症结所在。

最后,我尝试将输出函数声明为单独的类型缩写:

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

此操作失败,结果与上一次尝试相同。

如何说服编译器不要专门化 output 的类型并将其保留为 Printf.TextWriterFormat&lt;'a&gt; -&gt; 'a

【问题讨论】:

  • 你真的需要传递类似 printf 的函数本身吗?如果你想使用某个函数,它需要一个string,并且你想用格式化的字符串调用它,只需在你的函数中使用Printf.ksprintf
  • 您可能还会发现这很有用——在此文件 (Pervasive.fs) 的底部,我编写了一些函数,它们采用任意格式的字符串并在底层使用 Printf.ksprintf

标签: f# type-inference


【解决方案1】:

问题在于,当您说(output : Printf.TextWriterFormat&lt;'a&gt; -&gt; 'a) 时,这意味着“一些'a 的输出将Printf.TextWriterFormat&lt;'a&gt; 转换为'a”。相反,您想说的是“for all 'a 输出可以采用 Printf.TextWriterFormat&lt;'a&gt; 并返回 '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++ 很奇怪。感谢您解释差异。
【解决方案2】:

我认为 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 中的用法与我想要的最相似。
【解决方案3】:

使用 FSharp 的 inline 功能,您可以使用“工作”函数预先配置 Printf.ksprintf 函数,从而拥有一个接受格式字符串及其特殊必需参数的最终函数,类似于 printfsprintf。接受生成的string 的工作函数大概可以做它想做的任何事情,例如在这个将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)

...

【讨论】:

    猜你喜欢
    • 2014-01-27
    • 2021-08-02
    • 1970-01-01
    • 1970-01-01
    • 2011-10-31
    • 1970-01-01
    • 2019-07-25
    • 2021-12-26
    相关资源
    最近更新 更多