【发布时间】:2015-10-05 05:08:29
【问题描述】:
我读过一个类似的问题:Magic sprintf function - how to wrap it?,但我的要求有点不同,所以我想知道它是否可行。
首先,我想稍微解释一下场景,我目前有一个类似的跟踪功能
let Trace traceLevel ( fs : unit -> string) =
if traceLevel <= Config.TraceLevel then
Trace.WriteLine <| fs()
因此只有当traceLevel小于或等于Config.TraceLevel指定的跟踪级别时,才会调用函数“fs”来生成字符串。 所以当 traceLevel 大于 Config.TraceLevel 时,它是一个无操作。根本不评估“fs”。
虽然不限于,但在实践中,几乎所有用例看起来都像
Trace 4 (fun _ -> sprintf "%s : %i" "abc" 1)
总是写“fun _ -> sprintf”部分是相当乏味的。理想情况下,最好提供一种用户可以编写的风格
Trace 4 "%s : %i" "abc" 1
它可以
- 获取 sprintf 提供的格式/参数检查。
- 具有与采用 lambda "fs" 的原始跟踪函数相同的性能行为。这意味着如果对跟踪级别的检查返回 false,它本质上是一个空操作。无需支付额外费用(例如字符串格式化等)
即使在阅读了原始SO question 的答案后,我也无法弄清楚如何实现这一点。
似乎 kprintf 允许针对格式化字符串调用延续函数。包装器仍然返回由 printf 函数之一返回的函数(然后可以是一个带有一个或多个参数的函数)。所以柯里化可以发挥作用。但是,在上述情况下,需要在格式化字符串之前评估条件,然后将格式化的字符串应用到 Trace.WriteLine。似乎现有的 Printf 模块有一个 API 允许注入前置条件评估。因此,通过包装现有的 API 似乎并不容易。
关于如何实现这一点的任何想法? (我很简短地阅读了FSharp.Core/printf.fs,似乎可以通过提供新的派生 PrintfEnv 来做到这一点。但是,这些是内部类型)。
更新
感谢托马斯和林肯的回答。我认为这两种方法都会对性能造成一些影响。我用 fsi 在我的机器上做了一些简单的测量。
选项 1:我原来的方法,在“假”路径上,根本不评估“fs()”。用法不是很好,因为需要编写“fun _ -> sprintf”部分。
let trace1 lvl (fs : unit -> string) =
if lvl <= 3 then Console.WriteLine(fs())
选项 2:格式化字符串但将其丢弃在“假”路径上
let trace2 lvl fmt =
Printf.kprintf (fun s -> if lvl <= 3 then Console.WriteLine(s)) fmt
选项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> ())
现在我用类似的代码对这三个时间都进行了计时
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
这是我得到的:
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
因此,与选项 1(尤其是选项 3)相比,选项 2 和 3 的性能都受到了显着影响。如果字符串格式更复杂,这个差距会扩大。例如,如果我将格式和参数更改为
"%s: %i %i %i %i %i" (i.ToString()) i (i * 2) (i * 3) (i * 4) (i * 5)
我明白了
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
到目前为止,似乎仍然没有令人满意的解决方案来同时获得可用性和性能。
【问题讨论】:
-
仅供参考;您可以通过添加一些记忆(即缓存)来显着改进 latkins 建议。
标签: f#