如何在F#中有条件地包装sprintf?

Los*_*ion 6 f#

我读过一个类似的问题:Magic sprintf函数 - 如何包装它?,但我的要求有点不同,所以我想知道它是否可行.

首先,我想稍微解释一下这个场景,我目前有跟踪功能一样

let Trace traceLevel ( fs : unit -> string) =
    if traceLevel <= Config.TraceLevel then
        Trace.WriteLine <| fs()
Run Code Online (Sandbox Code Playgroud)

因此,仅当traceLevel小于或等于Config.TraceLevel指定的跟踪级别时,才会调用函数"fs"来生成字符串.因此,当traceLevel大于Config.TraceLevel时,它是一个无操作."fs"根本没有评估.

虽然不限于此,但在实践中,几乎所有用例都是如此

Trace 4 (fun _ -> sprintf "%s : %i"  "abc" 1)
Run Code Online (Sandbox Code Playgroud)

总是写"fun _ - > sprintf"部分是相当繁琐的.理想情况下,提供用户可以写的风味会很好

Trace 4 "%s : %i" "abc" 1
Run Code Online (Sandbox Code Playgroud)

它可以

  • 获取sprintf提供的格式/参数检查.
  • 具有与采用lambda"fs"的原始跟踪函数相同的性能行为.这意味着如果跟踪级别的检查返回false,则它本质上是一个无操作.不需要支付额外费用(例如字符串格式等)

在阅读了原始SO问题的答案后,我无法想象如何实现这一点.

似乎kprintf允许对格式化的字符串调用continuation函数.包装器仍然返回一个printf函数返回的函数(然后它可以是一个带有一个或多个参数的函数).因此,currying可以发挥作用.但是,在上面的情况中,需要的是在格式化字符串之前评估条件,然后将格式化的字符串应用于Trace.WriteLine.似乎现有的Printf模块具有允许注入前置条件评估的API.因此,通过包装现有API似乎不容易实现.

有关如何实现这一点的任何想法?(我非常简短地阅读了FSharp.Core/printf.fs,似乎可以通过提供新的派生PrintfEnv来实现.但是,这些是内部类型).

更新

感谢Tomas和Lincoln的答案.我认为这两种方法都会受到一些影响.我用fsi在我的机器上进行了一些简单的测量.

选项1:我的原始方法,在"假"路径上,"fs()"根本没有被评估.用法不太好,因为需要编写"fun _ - > sprintf"部分.

let trace1 lvl (fs : unit -> string) =
    if lvl <= 3 then Console.WriteLine(fs())
Run Code Online (Sandbox Code Playgroud)

选项2:格式化字符串,但将其丢弃在"假"路径上

let trace2 lvl fmt = 
    Printf.kprintf (fun s -> if lvl <= 3 then Console.WriteLine(s)) fmt
Run Code Online (Sandbox Code Playgroud)

选项3:通过递归,反射和框

let rec dummyFunc (funcTy : Type) retVal =
    if FSharpType.IsFunction(funcTy) then
        let retTy = funcTy.GenericTypeArguments.[1]
        FSharpValue.MakeFunction(funcTy, (fun _ -> dummyFunc retTy retVal))
    else box retVal

let trace3 lvl (fmt : Printf.StringFormat<'t, unit>) =
    if lvl <= 3 then Printf.kprintf (fun s -> Console.WriteLine(s)) fmt
    else downcast (dummyFunc typeof<'t> ())
Run Code Online (Sandbox Code Playgroud)

现在我把所有三个用代码计时

for i in 1..1000000 do
    trace1 4 (fun _ -> sprintf "%s:%i" (i.ToString()) i)

for i in 1..1000000 do
    trace2 4 "%s:%i" (i.ToString()) i

for i in 1..1000000 do
    trace3 4 "%s:%i" (i.ToString()) i
Run Code Online (Sandbox Code Playgroud)

这是我得到的:

trace1: 
  Real: 00:00:00.009, CPU: 00:00:00.015, GC gen0: 2, gen1: 1, gen2: 0
trace2:
  Real: 00:00:00.709, CPU: 00:00:00.703, GC gen0: 54, gen1: 1, gen2: 0
trace3:
  Real: 00:00:50.918, CPU: 00:00:50.906, GC gen0: 431, gen1: 5, gen2: 0
Run Code Online (Sandbox Code Playgroud)

因此,与选项1(尤其是选项3)相比,选项2和3都具有显着的性能.如果字符串格式更复杂,这个差距会增大.例如,如果我将格式和参数更改为

"%s: %i %i %i %i %i" (i.ToString()) i (i * 2) (i * 3) (i * 4) (i * 5)
Run Code Online (Sandbox Code Playgroud)

我明白了

trace1: 
  Real: 00:00:00.007, CPU: 00:00:00.015, GC gen0: 3, gen1: 1, gen2: 0
trace2:
  Real: 00:00:01.912, CPU: 00:00:01.921, GC gen0: 136, gen1: 0, gen2: 0
trace3:
  Real: 00:02:10.683, CPU: 00:02:10.671, GC gen0: 1074, gen1: 14, gen2: 1
Run Code Online (Sandbox Code Playgroud)

到目前为止,似乎仍然没有令人满意的解决方案来获得可用性和性能.

Tom*_*cek 2

技巧是使用该kprintf函数:

let trace level fmt = 
  Printf.kprintf (fun s -> if level > 3 then printfn "%s" s) fmt

trace 3 "Number %d" 10
trace 4 "Better number %d" 42
Run Code Online (Sandbox Code Playgroud)

您可以通过部分应用来使用它,这样 的格式字符串所需的所有参数都kprintf将成为您正在定义的函数的参数。

然后,该函数使用最终字符串调用延续,这样您就可以决定如何处理它。