如何在 F# 中使用 sprintf 构建格式字符串?

Tho*_*mas 3 f# string-formatting

我正在尝试从:

sprintf "%3.1f" myNumber
Run Code Online (Sandbox Code Playgroud)

到:

sprintf myFormatter myNumber
Run Code Online (Sandbox Code Playgroud)

这是不可能的

我遇到的情况是数字精度取决于某些设置,因此我希望能够创建自己的格式化程序字符串。

我知道可以使用 String.Format 来完成,但我很好奇是否有使用 sprintf 或 ksprinf 的 F# 方式;可以吗?

Abe*_*bel 5

简单回答

编辑:F# Slack 上的 Diego Esmerio 向我展示了一种更简单的方法,老实说,我在计算下面的答案时从未想到过。诀窍是PrintfFormat直接使用,如下所示。

// Credit: Diego. This 
let formatPrec precision = 
    PrintfFormat<float -> string,unit,string,string>(sprintf "%%1.%if" precision)

let x = 15.234

let a = sprintf (formatPrec 0) x
let b = sprintf (formatPrec 1) x
let c = sprintf (formatPrec 3) x
Run Code Online (Sandbox Code Playgroud)

输出:

val formatPrec : precision:int -> PrintfFormat<(float -> string),unit,string,string>
val x : float = 15.234
val a : string = "15"
val b : string = "15.2"
val c : string = "15.234"
Run Code Online (Sandbox Code Playgroud)

Expr这种方法可以说比下面基于 - 的方法简单得多。对于这两种方法,请小心格式化字符串,因为它可以很好地编译,但如果它无效,则会在运行时中断。

原始答案(复杂)

这并不是一件容易的事,因为像 和sprintf之类的函数printfn是编译时特殊情况函数,它们将字符串参数转换为函数(在本例中为 type float -> string)。

您可以使用 做一些事情kprintf,但它不允许格式化参数成为动态值,因为编译器仍然想要对其进行类型检查。

然而,使用引号我们可以自己构建这样的函数。最简单的方法是根据您的表达式创建报价并更改我们需要更改的部分。

出发点是这样的:

val formatPrec : precision:int -> PrintfFormat<(float -> string),unit,string,string>
val x : float = 15.234
val a : string = "15"
val b : string = "15.2"
val c : string = "15.234"
Run Code Online (Sandbox Code Playgroud)

这可能看起来很混乱,但由于我们只需要改变一点点,所以我们可以相当简单地做到这一点:

> <@ sprintf "%3.1f" @>
val it : Expr<(float -> string)> =
  Let (clo1,
     Call (None, PrintFormatToString,
           [Coerce (NewObject (PrintfFormat`5, Value ("%3.1f")), PrintfFormat`4)]),
     Lambda (arg10, Application (clo1, arg10)))
...
Run Code Online (Sandbox Code Playgroud)

要使用它,我们现在可以简单地执行以下操作:

open Microsoft.FSharp.Quotations   // part of F#
open Microsoft.FSharp.Quotations.Patterns  // part of F#
open FSharp.Quotations.Evaluator    // NuGet package (with same name)

// this is the function that in turn will create a function dynamically
let withFormat format = 
    let expr =
        match <@ sprintf "%3.1f" @> with
        | Let(var, expr1, expr2) ->
            match expr1 with
            | Call(None, methodInfo, [Coerce(NewObject(ctor, [Value _]), mprintFormat)]) ->
                Expr.Let(var, Expr.Call(methodInfo, [Expr.Coerce(Expr.NewObject(ctor, [Expr.Value format]), mprintFormat)]), expr2)

            | _ -> failwith "oops"  // won't happen

        | _ -> failwith "oops"  // won't happen

    expr.CompileUntyped() :?> (float -> string)
Run Code Online (Sandbox Code Playgroud)

或者像这样:

> withFormat "%1.2f" 123.4567899112233445566;;
val it : string = "123.46"

> withFormat "%1.5f" 123.4567899112233445566;;
val it : string = "123.45679"

> withFormat "%1.12f" 123.4567899112233445566;;
val it : string = "123.456789911223"
Run Code Online (Sandbox Code Playgroud)

现在格式字符串在编译期间是否是固定字符串并不重要。但是,如果这用于性能敏感区域,您可能需要缓存结果函数,因为重新编译表达式树的成本相当高。