F#中的printfn类型,静态与动态字符串


Type of printfn in F#, static vs dynamic string

我刚开始在Mono中玩弄f_,接着出现了我无法完全理解的问题。查找有关printfnTextWriterFormat的信息也没有带来启迪,所以我想我会问这里。

在FSI中,我运行以下程序:

1
2
3
4
5
>"hello";;
val it : string ="hello"
> printfn"hello";;
hello
val it : unit = ()

只是一个普通的字符串并打印出来。好的。现在,我想声明一个变量来包含相同的字符串并打印它:

1
2
3
4
> let v ="hello" in printfn v ;;
let v ="hello" in printfn v ;;
---------------------------^
\...\stdin(22,28): error FS0001: The type 'string' is not compatible with the type 'Printf.TextWriterFormat<'a>'

我从阅读中了解到,printfn需要一个常量字符串。我也知道我可以用类似于printfn"%s" v的东西来解决这个问题。

不过,我想知道这里的打字是怎么回事。显然,"hello"string型,v也是。那为什么会有类型问题呢?printfn有什么特别的吗?据我所知,编译器已经对第一个字符串的参数执行了类型检查,例如printfn"%s" 1失败。当然,这不适用于动态字符串,但我认为对于静态情况,这只是编译器方面的一个便利。


好问题。如果您查看printfn的类型,即Printf.TextWriterFormat<'a> -> 'a的类型,您将看到编译器在编译时自动将字符串强制为TextWriterFormat对象,从而推断出适当的类型参数'a。如果要将printfn与动态字符串一起使用,您可以自己执行转换:

1
2
3
4
5
6
7
8
let s = Printf.TextWriterFormat<unit>("hello")
printfn s

let s' = Printf.TextWriterFormat<int -> unit>("Here's an integer: %i")
printfn s' 10

let s'' = Printf.TextWriterFormat<float -> bool -> unit>("Float: %f; Bool: %b")
printfn s'' 1.0 true

如果字符串是静态的(如上述示例中所示),那么编译器仍然可以将正确的泛型参数推断为TextWriterFormat,而不是调用构造函数:

1
2
3
let (s:Printf.TextWriterFormat<_>) ="hello"
let (s':Printf.TextWriterFormat<_>) ="Here's an integer: %i"
let (s'':Printf.TextWriterFormat<_>) ="Float: %f; Bool: %b"

如果字符串是真正动态的(例如,它是从文件中读取的),那么您需要显式地使用类型参数并像前面的示例中那样调用构造函数。


这只和你的问题有点关系,但我认为这是一个很好的技巧。在C中,我经常使用模板字符串与存储为常量的String.Format一起使用,因为它可以使代码更清晰:

1
String.Format(SomeConstant, arg1, arg2, arg3)

而不是。。。

1
String.Format("Some {0} really long {1} and distracting template that uglifies my code {2}...", arg1, arg2, arg3)

但由于printf方法家族坚持使用文字字符串而不是值,所以我最初认为,如果我想使用printf,就不能在f中使用这种方法。但后来我意识到f有更好的应用——部分函数。

1
let formatFunction = sprintf"Some %s really long %i template %i"

它刚刚创建了一个函数,该函数接受一个字符串和两个整数作为输入,并返回一个字符串。也就是说,string -> int -> int -> string。它甚至比常量string.format模板更好,因为它是一个强类型方法,允许我重新使用模板而不将其包含在内联中。

1
let foo = formatFunction"test" 3 5

我使用f的次数越多,发现部分函数应用的用途就越多。很棒的东西。


我认为,当在printfn"hello"的上下文中使用时,文字值"hello"是String类型是不正确的。在此上下文中,编译器推断文字值的类型为Printf.TextWriterFormat

起初,我觉得奇怪的是,一个文本字符串值会有一个不同的推断类型,这取决于它被使用的上下文,但当然,我们在处理数字文本时习惯了这一点,数字文本可能表示整数、小数、浮点数等,这取决于它们出现的位置。

如果要在通过printfn使用变量之前声明该变量,可以使用显式类型声明该变量…

1
let v ="hello" : Printf.TextWriterFormat<unit> in printfn v

…或者可以使用EDOCX1的构造函数(9)将普通字符串值转换为必要的类型…

1
2
let s ="foo" ;;
let v = new Printf.TextWriterFormat<unit>(s) in printfn v ;;


正如您正确观察到的,printfn函数采用的是"printf.textWriterFormat<'a>",而不是字符串。编译器知道如何在常量字符串和"printf.textWriterFormat<'a>"之间转换,但不知道如何在动态字符串和"printf.textWriterFormat<'a>"之间转换。

这就引出了一个问题,为什么它不能在动态字符串和"printf.textWriterFormat"之间转换。这是因为编译器必须查看字符串的内容,并确定其中包含哪些控制字符(即%s%i等),由此计算出"printf.textWriterFormat<'a>"类型参数的类型(即"a位")。这是一个由printfn函数返回的函数,意味着printfn接受的其他参数现在是强类型的。

要在示例"printfn"%s"中稍微清楚一点,将"%s"转换为"printf.textwriterformat unit>",这意味着"printfn"%s"的类型是string->unit。